C# Garbage Collector 1

Featured image

Memory Flow


닷넷의 가비지 컬렉터(이하 GC)을 살펴보기 전 GC가 없는 C/C++는 메모리 관리가 어떻게 되고 있는지 살펴보자.

C/C++ 환경에서의 메모리 할당/해제는 프로그래머의 몫으로, 힙에 자유메모리 블록을 런타임 라이브러리가 유지함으로써 관리되었다.

런타임 라이브러리는 힙 상에서 사용 가능한 메모리 블럭을 리스트로 유지하며, 메모리의 할당(new, malloc)이 있을 경우 해당 리스트에서 요청된 크기의 메모리 검색하여 찾으며 메모리가 해제(delete, free)되면 할당된 메모리는 다시 사용 가능한 메모리 리스트에 삽입된다.

C/C++의 메모리 관리 방식은 메모리 할당에 소요되는 시간이 상대적으로 길다. 메모리 할당/해제가 반복적으로 일어남에 따라 힙의 메모리 사용 패턴은 조각나기 쉽고 조각난 메모리는 메모리 할당 시 메모리 블록을 검색해야 하는 오버헤드를 갖기 때문이다.

메모리 해제 또한 오버헤드를 갖게 되는데, 메모리가 해제 될 때 인접한 자유 메모리 블록을 검사하여, 존재한다면 메모리 블럭을 병합하여 보다 큰 메모리 블록으로 만들어야하기 떄문이다.

다음 그림은 C/C++에서의 메모리 할당/해제 과정을 위 설명의 과정대로 표시한 그림이다.

gc1

C# Garbage Collector 기본 작동 방식 🚙

닷넷의 GC의 작동 방식은 선형 메모리 할당과 사용하지 않는 메모리 블록을 찾아 제거하는 형태로 이루어 진다.

선형 메모리는 C/C++과 같이 자유 메모리 블록 리스트를 사용하지 않고 다음 메모리 할당을 위한 포인터 만을 유지하는 것을 얘기한다.

닷넷에서, 객체에 대한 메모리 할당이 이루어지면 포인터 값을 할당할 크기만큼 증가 시키고 할당한 메모리를 0으로 초기화한다.

다음 그림은 .Net에서의 메모리 할당 과정을 표시한 그림이다.

gc2

GC의 메모리 할당 시 자유 메모리 블록을 검사할 필요가 없고, 단순히 포인터 값을 증가 시키는 것이기 때문에 C/C++의 메모리 할당과 비교해 보았을 때 매우 빠르다. (닷넷에서 메모리 할당 시 메모리 조각이 발생하지 않는다. 이로 인해 닷넷 환경에서는 다양하고 많은 객체들이 힙상에 존재하게 되는데 이렇게 잦은 메모리 할당에서 최적의 성능을 내기 위해서는 메모리 조각이 발생하지 않는 방식의 메모리 할당을 사용한다.)

GC에서의 메모리 해제는 GC의 특정 조건을 만족하는 상황이 되면 현재 수행중인 쓰레드를 모두 중단시키고 GC 쓰레드를 활성화 시킨다.

GC 쓰레드는 힙 상에서 사용 중인 객체들의 참조 그래프를 생성하고 사용 중인 객체의 위치를 재조정함으로써 사용하지 않는 객체들을 힙 상에서 제거한다.

참조 그래프를 만들기 위해서는 루트 참조가 필요한데, 루트 참조에 해당하는 것은 현재 각 쓰레드가 수행중인 메서드의 스택 변수, 레지스터 변수, 정적 필드, 정적 변수 등이다. 이 루트 참조를 시작점으로하여 각 루트 참조가 참조하는 객체, 그리고 다시 그 객체가 참조하는 다른 객체들의 참조 그래프를 추가함으로써 현재 사용중인 객체의 그래프를 작성한다.

참조 그래프가 완성되면 이 그래프에 포함되지 않는 객체는 사용 중이 아닌 객체(참조가 없는 객체)로 판단하여 가비지 컬렉션의 대상이 되는 것이다. GC의 작업은 참조 그래프상의 객체들을 힙 상에서 재배치하고 메모리 할당 포인터를 감소 시키는 작업을 한다. 이러한 과정을 메모리 컴팩션이라고 한다.

다음 그림은 .Net에서의 메모리 해제 과정을 표시한 그림이다.

gc3

객체의 참조값을 변경하는 것이 작업 소요 시간이 오래걸릴 것 같지만 GC는 참조 그래프를 생성하면서 필요한 정보를 갖고 있기 떄문에 생각보다 느리지 않게 작업이 이루어 진다.


Reference

가비지 컬렉션 다시 보기 Part I