C++ & C#

[C#] 가비지 컬렉터

송만덕 2021. 11. 22. 21:43

흔히 .NET 기반의 환경을 Managed 환경이라고 한다.

.NET은 메모리를 할당하고, 해제하는 부분을 자동으로 관리해주기 때문이다.

이를 가능하게 하는 것은 바로 .NET의 CLR에 가비지 컬렉터의 존재인데,

가비지 컬렉터가 사용하지 않는 객체를 알아서 판단하여 메모리를 회수하기 때문이다.

GC 또한 소프트웨어이기 때문에 CPU와 메모리를 사용한다. 따라서 최소한으로 자원을 사용하게 하는 것이 중요하다.

 


가비지 컬렉터와 가비지 컬렉션

 

가비지 컬렉터는 효율적인 메모리 관리를 하기 위해 힙 영역에서 가비지를 찾아내고

특정 기준으로 메모리 할당을 해제하기 위해 만들어진 것이다.

가비지 컬렉터는 힙 영역에 있는 객체를 사용되는 (Reachable)과 사용되지 않는 (Unreachable) 상태로 구분한다.

 

가비지 컬렉션은 메모리를 해제하고 새롭게 재배치 하는 작업을 칭하며 자동으로 수행되지만, 수동으로도 수행 명령을 통해 가능하다.

가비지 컬렉션은 시스템의 실제 메모리가 부족하거나, 힙의 할당된 개체에 사용되는 메모리가 허용되는 임계값을 초과하거나, GC.Collect 메서드가 호출되거나 하는 세 가지 조건 중 하나가 충족될 경우 발생한다.

 

C#으로 작성한 프로그램을 실행하면 CLR은 프로그램을 위한 일정 크기의 메모리를 확보한다.

일정 메모리 공간을 확보해서 하나의 Managed Heap을 마련하게 되고, 객체를 할당하게 되면 메모리에 순서대로 할당하게 된다.

참조 형식의 객체가 할당될 때는 스택 영역에는 힙의 메모리 주소를, 힙 영역에는 실제 값이 할당되고

할당된 코드블록이 끝나면 스택 영역의 메모리가 회수되고, 힙 영역의 값은 쓰레기값이 되는데 회수된 스택의 객체를 '루트'라고 부른다.

.NET 응용 프로그램이 실행되면 JIT 컴파일러가 이 루트들을 목록으로 만들고 CLR은 이 루트 목록을 관리하며 다음과 같이 상태를 갱신하게 된다.

가비지 컬렉터는 힙영역의 임계치에 다다르게 되면
1. 모든 객체가 쓰레기라고 가정한다. (루트 목록 내의 어떤 루트도 메모리를 가르키지 않는다)
2. 루트 목록을 순회하며 참조하고 있는 힙 객체와의 관계 여부를 조사한다. (Mark)
3. 쓰레기가 차지하고 있던 메모리가 회수되면 인접 객체들을 이동시켜 채워가며 정리. (Sweep & Compaction)

 

CLR의 메모리도 구역을 나누어 메모리에서 빨리 해제될 객체와 오래 있을 객체를 따로 담아 관리한다.

구체적으로 메모리는 0,1,2 세대로 나누어지고 관리하는 방법은 다음과 같다.

 

응용 프로그램을 실행하면 0세대부터 할당된 객체들을 채우기 시작.
1. 0세대 가비지 컬렉션 임계치에 도달하면 0세대에 대해 가비지 컬렉션 수행. 여기서 남아있는 객체는 1세대로 옮겨짐.
2. 1번 과정을 반복하다 보면 1세대 가비지 컬렉션이 임계치에 도달하게 되고, 1번 과정과 같은 수행이 이루어짐.
3. 2번 과정 또한 반복되면 2세대 가비지 컬렉션이 임계치에 도달하게 되고, 2세대 가비지 컬렉션이 임계치에 도달하게 되면 세대 전체 가비지 컬렉션을 수행한다.

 

한 번 사용한 객체는 꾸준히 많이 사용하지만, 그렇지 않은 객체는 단기적으로 사용을 많이 하지 않기 때문에

0세대 일수록 컬렉션이 많이 발생한다

프로그램이 오래 살아남는 객체들을 마구 생성하면 2세대 힙이 가득차게 될 것이고

2세대 힙이 가득차게 되면 CLR은 응용프로그램의 실행을 멈추고 전체 가비지 컬렉션을 수행하며 메모리를 확보하려 하기 때문에

응용 프로그램이 차지하는 메모리가 많을 수록 프로그램이 정지하는 시간도 늘어나게 된다.

(Mark 단계에서 전체 스레드가 멈춘다. 흔히 가비지 컬렉션에서 성능 저하를 언급하는 것은 바로 객체 탐색과 스레드 멈춤 때문.)


Mark-Sweep-Compaction

Mark : 사용되는 객체를 표시하는 작업.

Sweep : 사용되지 않는 객체를 제거하는 작업.

Compaction : 파편화 된 메모리 영역을 앞에서부터 채워나가는 작업


효율적인 메모리 사용

 

-GC.Collect()-

앞서 GC.Collect() 메서드가 호출되면 가비지 컬렉션이 수행된다고 했지만, 이는 가급적 피하는 것이 좋다.

이유는 이렇다.

 

1. 가비지 컬렉션이 수행하는 메커니즘이 생각보다 간단하지 않기 때문에 성능상 부하가 생긴다.

현재 객체가 사용 중인지 확인하는 작업이 필요하며, 그 작업이 끝난 후 객체를 하괴하게 된다.

객체를 파괴할 때에는 참조되고 있는 객체 역시 파괴해야 한다.

뿐만 아니라 파괴된 객체들의 빈자리를 메꾸기 위해 가비지 컬렉터는 객체들의 재배치(Compaction)를 하게 되므로 메모리가 많을 수록 부하가 크다.

 

2. 객체가 승격된다.

객체가 0세대에서 승격되면 그 객체가 자주 사용되지 않더라도 자동적으로 객체가 정리될 확률이 줄어든다.

 

 

-사용하지 않는 객체는 가비지 컬렉터가 수집할 수 있는 대상으로 설정-

1. 사용하지 않는 객체에 NULL을 대입.

2. using을 사용하여 객체를 사용하는 범위를 지정.

 

-자주 사용하는 객체는 전역변수로 잡아 가비지를 최소화.-