원문: .Net Garbage Collection Documents
번역자: 박성국

Garbage Collection

.NET의 Garbage Collector는 애플리케이션의 메모리의 할당과 해제를 관리한다. 프로그래머가 새로운 객체를 생성할 때마다, CLR(Common Language Runtime)은 매니지드 힙 영역(managed heap) 해당 객체를 위한 메모리를 할당한다. 이 힙 영역에 가용 메모리가 남아있는 한, CLR은 새로운 객체에 대한 공간을 계속 할당한다. 그러나 메모리는 무한한 것이 아니다. 결국 Garbage Collector는일부 메모리를 해제하기 위한 Colllection를 실행해야만 한다. Garbage Collector의 최적화된 엔진은 현재 할당된 메모리에 기반해 Collection을 수행하기 위한 최적의 시점을 결정한다. Garbage Collector는 Collection을 수행할 때, 매니지드 힙 영역에 존재하지만 더는 사용되는 않는 객체들을 확인하고 메모리를 회수하기 위해 필요한 동작들을 수행한다.

Garbage Collection의 기초

CLR(Common Language Runtime)에서 Garbage Collector는 자동화된 메모리 관리자로 기능한다. Garbage Collector는 다음의 이점들을 제공한다:

  • 프로그래머가 메모리를 직접 해제하지 않고도 애플리케이션을 개발할 수 있도록 해준다.
  • 매니지드 힙 영역의 객체들을 효율적으로 할당한다.
  • 더는 사용되지 않는 객체들을 회수하며, 메모리를 해제하며, 후에 해당 메모리에 할당할 수 있도록 한다. 매니지드 객체들은 이처럼 초기화된 공간을 할당 받기 때문에 각 생성자에서 매 데이터 필드를 초기화할 필요가 없다.
  • 한 객체가 다른 객체의 공간을 사용하지 않도록 보장함으로써 메모리 안정성을 제공한다.

메모리의 기초

다음 리스트는 CLR의 메모리 개념들을 요약한 것이다.

  • 각 프로세스는 각각의 가상 주소 공간을 갖는다. 한 컴퓨터의 모든 프로세스는 같은 가상 메모리를 공유하며, 페이지 파일이 존재한다면 모두 그것을 공유한다.
  • 기본적으로 32-bit 컴퓨터에서 각 프로세스는 2 GB의 사용자-모드 가상 주소 공간을 갖는다.
  • 애플리케이션 개발자로서, 프로그래머는 가상 주소 공간만을 가지고 일하며 결코 물리적 메모리를 직접 조작할 수 없다. 가비지 컬렉터는 프로그래머 대신 매니지드 힙 상에서 가상 메모리를 할당하고 해제한다.
    만약 프로그래머가 네이티브 코드(native code)를 작성한다면 Win32 함수들을 가지고 가상 공간 상에서 작업을 해야 한다. 이 함수들은 네이티브 힙 상에서 가상 메모리를 할당하고 해제한다.
  • 가상 메모리는 다음 세 가지 상태에 놓일 수 있다:
    • 할당 가능(Free): 해당 메모리 블록은 참조되어 있지 않으며 따라서 해당 영역에 대한 할당이 가능하다.
    • 예약됨(Reserved): 해당 메모리 블록은 프로그래머가 사용 가능하도록 배정된 상태이며 해당 영역에 대한 다른 할당 요청은 받지 않는다. 그러나, 커밋(commit)되지 않은 상태에서 프로그래머는 이 메모리 블록에 데이터를 저장할 수는 없다.
    • 커밋됨(Committed): 해당 메모리 블록은 물리적 저장공간에 배정되어 있는 상태이다.

가상 메모리 공간은 파편화(get fragmented)될 수 있다. 이는 주소 공간에 구멍(holes)이라고 부르는 할당 가능한 메모리 공간들이 존재하는 것을 말한다. 가상 메모리 할당이 요청되는 경우, 가상 메모리 관리자는 해당 할당 요청을 충족시킬만큼 충분히 큰 메모리 블록을 찾는다. 2 GB의 할당 가능한(free) 영역이 있다고 해도 2 GB에 해당하는 단일한 주소 블록이 존재하지 않는다면 2 GB의 할당 요청은 실패할 수 있다.

프로그래머는 새로 예약할 가상 주소 공간이나 물리적인 공간이 부족한 경우 메모리 부족을 겪을 수 있다.

페이지 파일은 물리적 메모리의 압박(물리적 메모리의 요구)이 낮은 경우에도 사용된다. 물리적 메모리 압박이 높은 경우, 운영체제(이하 OS)는 데이터를 저장하기 위한 물리적인 메모리 공간을 제공해야 하며, 이는 물리적 메모리 공간에 있던 데이터의 일부를 페이지 파일로 백업한다. 해당 데이터는 필요해지기 전까지는 페이지 되지 않으며, 따라서 물리적 메모리 압박이 매우 낮은 경우에는 페이징 현상을 겪을 수 있다.

Garbage Collection의 조건

Garbage Collection은 다음 조건들 중 하나가 충족되면 발생한다:

  • 시스템의 물리적 메모리가 부족한 경우 발생한다. 이는 OS의 메모리 부족 알림 또는 호스트의 메모리 부족 신호에 의해 감지된다.
  • 매니지드 힙 상에 할당된 메모리가 임계점(threshold)를 넘는 경우. 이 임계점은 프로세스가 동작하는 내내 조정된다.
  • GC.Collect 메서드가 호출되는 경우. Garbage Collector는 끊임없이 동작하므로 대부분의 상황에서 프로그래머는 이 메서드를 호출할 필요가 없다. 이 메서드는 아주 특별한 상황 또는 테스팅에서 주로 사용된다.

매니지드 힙 (Managed Heap)

CLR에 의해 Garbage Collecor가 초기화된 이후에 Garbage Collector는 메모리를 보존하고 객체를 관리하기 위한 메모리 구간 하나를 할당한다. 이 메모리를 운영체제 상의 네이티브 힙에 대비하여 매니지드 힙이라고 부른다.

각각의 관리되는 프로세스는 하나의 매니지드 힙을 갖는다. 프로세스의 모든 스레스들은 같은 힙 공간에 객체들을 할당하게 된다.

메모리를 예약하는 경우, Garbage Collectgor는 관리되는 애플리케이션들을 위해 Win32 VirtualAlloc 함수를 호출하며 이는 한번에 하나의 메모리 구간을 예약한다. Garbage Collector는 또한 필요한 경우 메모리 구간들을 예약하고 Win32 VirtualFree 함수를 호출함으로써 이를 해제해 운영체제에 되돌려주게 된다 (객체를 초기화한 이후).

중요

Garbage Collector에 의해 할당되는 구간의 크기는 구현에 따라 다르며, 주기적인 갱신을 포함해 언제든 변경될 수 있는 종류의 것이다. 프로그래머는 애플리케이션 구동에 있어 결코 할당되는 메모리 크기를 추측해서도 안 되고 할당 가능한 메모리를 변경하려고 해서도 안 된다.

힙에 할당된 메모리가 적을 수록, Garbage Collector가 할 일은 더 적어진다. 프로그래머는 객체를 할당할 때 필요 이상의 객체들을 할당하지 않도록 한다. 예컨대, 15 bytes의 메모리가 필요할 때 32 bytes의 배열을 할당하지 않도록 한다.

Garbage Collection이 발동하면, Garbage Collector는 죽은 객체에 의해 점유 중이던 메모리를 수거한다. 이 수거 프로세스는 생존한 객체들을 압축해 해당 객체들을 옮길 수 있도록 한다. 죽은 객체들이 제거되면 힙을 점유하는 메모리는 줄어들게 된다. 이는 객체들이 매니지드 힙에 인접해 할당되고 인접해 존재하도록 함으로써 객체들의 지역성(locality)를 보장하기 위함이다.

Garbage Collection의 개입성(instrusiveness, 그 주기나 간격에 있어)은 매니지드 힙에 할당되어 생존한 메모리의 크기에 따라 결정된다.

이 매니지드 힙은 두 개의 힙이 겹쳐진 것으로 생각할 수 있다: 대형 객체 힙(LOH, Large Object Heap)과 소형 객체 힙(SOH, Small Object Heap)이 바로 그것이다.

LOH은 85,000 bytes 또는 그 이상의 객체들을 수용한다. LOH에 수용되는 객체는 일반적으로 배열이다. 한 객체의 인스턴스가 이정도로 큰 것은 드문 일이다.

세대

힙은 오래 생존한(long-lived) 객체와 짧게 생존한(short-lived) 객체를 분리해 다룰 수 있도록 세대로 구성되어 있다. Garbage Collection은 일반적으로 힙의 작은 부분을 차지하는 짧게 생존한 객체들을 수거하는 역할을 한다. 힙에는 총 3개의 세대가 존재한다.

  • 세대 0. 이는 가장 젊은 세대로서 짧게 생존한 객체들을 포함한다. 예를 들어, 임시 변수를 세대 0의 일원으로 볼 수 있다. Garbage Collection은 해당 세대를 대상으로 가장 자주 발생한다.

    새로 할당된 객체는 해당 객체가 대형 객체가 아닌 한 암묵적으로(implicitly) 세대 0에 포함된다. 만일 해당 객체가 대형 객체인 경우는 LOH으로 이동하며 세대 2에 포함되게 된다.

    대부분의 객체들은 세대 0일 때 Garbage Collection에 의해 수거되며 다음 세대까지 생존하지 않는다.

  • 세대 1. 이 세대는 짧게 생존한 객체들을 포함하며, 보다 짧게 생존한 객체들과 오래 생존한 객체들 사이에 일종의 버퍼(buffer)로 기능한다.

  • 세대 2. 이 세대는 오래 생존한 객체들을 포함한다. 오래 생존한 객체의 예로서는, 프로세스 내내 생존해야 하는 서버 애플리케이션의 정적(static) 데이터를 예로 들 수 있다.

Garbage Collection은 조건에 부합하는 한 특정 세대에서만 발생한다. 세대를 수거한다(collecting)는 것은 해당 세대와 그보다 짧게 생존한(젊은) 모든 세대를 수거한다는 것을 의미한다. 세대 2에 대한 Garbage Collection은 Full Garbage Collection이라고도 부르며, 이 경우에는 모든 세대의 모든 객체들에 대한 메모리를 수거하게 된다 (여기서 수거되는 객체들은 매니지드 힘의 모든 객체들에 해당한다).

세대별 생존과 승격

Garbage Collection에 의해 수거되지 않은 모든 객체들은 생존자(survivors)라고 불리며 이들은 다음 세대로 승격되게 된다. 세대 0에서 생존한 객체들은 세대 1이 된다. 세대 1에서 생존한 객체들은 세대 2가 된다. 세대 2에서 생존한 객체들은 세대 2에 머물게 된다.

Garbage Collector가 한 세대 내에서 생존률이 높다는 사실을 감지하게 되면, Garbage Collector는 해당 세대에 대한 할당 임계치(threshold)를 높이게 된다. 이에 따라, 그 다음 할당에 있어 해당 세대는 상당한 양의 수거된 메모리를 확보하게 된다. CLR은 두 가지의 우선순위의 균형을 끊임없이 추구한다: 애플리케이션의 작업대가 너무 커지는 것을 막고 Garbage Collection의 소요 시간이 너무 길어지는 것을 방지하는 것 사이에서 말이다.

<계속>