본문 바로가기

Flex

Flex, Flash 가비지 컬랙션 방법과 메모리 관리


이 글은 자꾸 까먹어서 정리겸 이해하고 있는 수준에 맞게 풀어서 쓴 글이다..
-------------------------

Flash나 Flex의 메모리는 개발자가 그냥 지울수가 없게 되어있다.
자바도 마찬가지고 VM기반의 언어에서는 메모리를 할당하고 해제 하는 과정이 시스템에서 알아서 하도록 되어있기 때문에.. 언제 메모리가 해제 되는지 개발자가 컨트롤 할수가 없다.

개발을 한 프로그램을 돌리기 시작했는데.. 메모리가 사용할수록 증가 한다면 그 프로그램은 결국에 가서는 시스템에 문제를 일으키고 종료될 것이다.

메모리 관리는 Garbage Collector (이하 GC) 라는 놈이 하게 되는데 말그대로 쓰레기를 수거하는 역할이다.
더이상 프로그램에서 사용하지 않는 객체들을 초기화하고 메모리를 시스템으로 반환해주게 된다.

플레시 플레이어는 객체가 생성되면 시스템에 메모리를 요청하게 되는데 이때 이후에 생길 객체들을 위해서 객체의 메모리보다 더 많은 양을 할당 받게 된다.
이 메모리가 부족해지면 다시 시스템에 더 많은 메모리를 요구 하게 되는데 이때 GC 가 일을 하게 된다. 쓸모 없는게 있는지 찾고 있으면 지우고 부족분에 대한 메모리를 요청하게 되는것이다.

개발자가 removeChild 나 null 을 선언할때 메모리가 해제되지 않는다는것이 중요하다.

GC 가 돌면서 사용하지 않는 것을 찾는 방법은 두가지 룰이 있다.

1. 레퍼런스 카운팅 (Reference Counting)
2. 마크 앤 스윕 (Mark and Sweep)


레퍼런스 카운팅은 해당 객체가 참조하고 있거나 해당 객체를 참조하고 있는 놈이 있는지 찾는다. 해당 객체를 참조하고 있는 애들을 찾아서 있으면 카운트를 1 올리는 방식이다.

전부 찾아보고 나서 카운트가 0 인 객체들을 메모리에서 해제한다.

함수에서 Label 을 만든다

var label:Label = new Label();


new 키워드로 인스턴스를 생성했으니 메모리 공간을 할당 받는다. 하지만 참조하고 있는 곳이 없다. 따라서 GC 가 실행되면 label 은 메모리에서 사라진다.

라벨을 만들고 어플리케이션에 추가한다.

var label:Label = new Label();this.addChild(label);


함수 안에서 만들어졌지만 어플리케이션에 추가 되었다.

어플리케이션은 child 로 label 을 참조하고 있고, label은 parent로 어플리케이션을 참조하게 된다.

this.removeChild(label);


하게 되면 참조 관계가 사라지고 레퍼런스 카운팅 값이 0 이 되어 사라질수 있게 된다.

어플리케이션안에 캔버스를 만들고 캔버스 안에 라벨을 만든다.

var canvas:Canvas = new Canvas();this.addChild(canvas);var label:Label = new Label();canvas.addChild(label);


어플리케이션이 child로 캔버스를 참조하고 있고 label은 parent 로 참조하고 있으니 캔버스의 레퍼런스 카운팅은 2 이다.
라벨은 캔버스와 관계가 있으니 카운팅은 1 이된다.

이때 캔버스를 삭제한다.

this.removeChild(canvas);


어플리케이션에서는 캔버스가 사라지고 아무것도 없다. 과연 캔버스 객체는 메모리에서 사라질까?
화면에서 사라졌지만.  캔버스와 라벨은 여전히 레퍼런스 카운팅 값이 1 이다. 상호참조 하고 있기 때문에 레퍼런스 카운팅 기법만으로는 여전히 삭제가 되지 않는다.

이러한 상호참조 문제를 해결하기 위해서 사용되는 방법이 두번째의 마크 앤 스윕이다.

마크 앤 스윕 기법은 어플리케이션에서부터 하위로 참조되고 있는 객체들을 찾아서 마크한다. 어플리케이션에서 부터 참조를 체크 한다는 것이 중요하다.
쭉 마크를 하고 모든 뎁스에 대해서 마크가 끝나면 마크가 없는 애들은 지운다.
마크 앤 스윕 방법은 레퍼런스 카운팅 보다 작업하는 시간이 더 걸리고 시스템에 부하도 많이 주기 때문에 자주 실행되지는 않는다고 한다.

위의 캔버스는 어플리케이션에서 참조되고 있지 않기 때문에 메모리를 반환하고 생을 마감하게 된다.

중요한 것은 위의 방법을 이해하고 사용하지 않는 것을 삭제 할때에 다음번 GC가 삭제 된것을 전부 쓸어갈수 있도록 코딩을 해야 메모리가 무한정 증가하다가 뻗는것을 막을수 있다.

그럼 메모리를 반환하는데 있어서 걸림돌이 되는것들이 무엇이 있을까.
가장 문제가 되는 것이 이벤트 리스너다.

객체지향으로 설계 하다보니 이벤트를 엄청나게 사용하게 되고 addEventListener가 컴포넌트를 만들다 보면 대여섯개씩 기본으로 붙는 경우가 허다하다.

이벤트는 기본적으로 이벤트를 디스패치한 객체의 참조를 가지고 날아가게 된다.
따라서 event.target 또는 currentTarget 으로 이벤트를 발생시킨 놈을 사용할 수 있는것이다.

어떤 컴포넌트가 이벤트를 리슨 하거나 디스패치 하게되면 그 객체는 레퍼런스 카운팅이 1이상으로 유지 되기 때문에 살아있는 이벤트가 하나라도 있으면 삭제가 되지 않는 문제가 발생한다.

따라서 addEventListener 해준것은 반드시 removeEventListener 해주는 습관을 들여야 한다.

지워지는 시점이 명확하지 않다거나 내부에서 참조되서 관리가 힘들다거나 하는 경우에는 언제 remove 해줘야 하는지 결정하기가 쉽지 않다.
이럴때 사용하는것이 useWeakReference 이다.

addEventListener (type:String, listener:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false):void
리스너에서 이벤트 알림을 받을 수 있도록 EventDispatcher 객체에 이벤트 리스너 객체를 등록합니다.

다섯번째 파라미터를 true 로 해주면 참조값을 약하게 잡고 있게 되고 이후에 GC 에서 삭제해줄 확률이 높아진다. 확률만 높아진다는 것이지 꼭 사라진다는건 아니기 때문에. removeEventListener 해주는 것이 가장 좋다.

이벤트쪽만 정리가 잘 되어있어도 메모리누수를 상당부분 막을수 있고.

플렉스IDE 를 사용한다면 개발중간중간에 프로파일러를 돌려서 체크 해보는 습관을 갖는게 좋을것같다.

나같은 경우는 커스텀 컴포넌트를 만들게 되면 destory() 함수를 만들어서 그 컴포넌트에서 사용되었던 이벤트 리스너를 일괄로 삭제하고 가능하면 컴포넌트 내부에 addChild 되어있던 것들도 삭제 해주는 함수를 만들어서 사용하고 있다.
removeChild 하기 전에 destory()를 실행시켜주고 삭제하면 기본적인 방지책은 되는것 같다.

PS. 메모리 관리에 좋은 방법들이 있으면 공유를 부탁드립니다~ 댓글 트랙백 환영