ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • C# 가비지컬렉션
    프로그래밍/기록, 개념, 용어 2022. 12. 11. 19:16
    반응형

    세대

    참조형 인스턴스들은 관리 힙에 생성되고, 관리 힙은 참조형 인스턴스들을 세대로 나눠서 관리한다.

    참조형 인스턴스는 관리 힙에서 0세대로 시작하고, 최대 2세대 까지 된다.

    또한, GC의 static 프로퍼티인 MaxGeneration로도 최대 세대를 알 수 있다.

    // 최대 세대를 반환.
    Console.WriteLine($"Max Gen: {GC.MaxGeneration}");

     

    처음 할당된 참조형 개체는 관리 힙 0세대에 생성된다.

    그리고 이 0세대에 생성된 개체들은 가비지 컬렉션이 일어날 때 이 개체를 참조하는 개체가 아무것도 없다면 제거되고, 제거되지 않는다면 1세대가 된다.

    아래는 참조형 개체를 하나 생성하고, 강제로 가비지 컬렉션을 실행하여 세대 증가를 확인하는 코드이다.

    string message = "Hello World!";
    
    // 0세대.
    Console.WriteLine($"Gen: {GC.GetGeneration(message)}");
    
    GC.Collect();
    
    // 1세대.
    Console.WriteLine($"Gen: {GC.GetGeneration(message)}");

     

    개체들의 세대 증가는 대략 아래의 맥락으로 이루어진다.

    관리 힙의 0세대 개체들이 임계 값을 넘어서면 가비지 컬렉션이 일어난다.
    (임계 값은 프로그램이 실행될 때 동적으로 조정된다.)
    그리고 0세대에서 제거되지 않는 개체는 1세대가 된다.

    1세대 개체들이 임계 값을 넘어서면 0세대, 1세대에 가비지 컬렉션이 일어난다.
    그리고 0세대에서 제거되지 않는 개체는 1세대가 되고, 1세대에서 제거되지 않는 개체는 2세대가 된다.

    2세대 개체들이 임계 값를 넘어서면 0세대, 1세대, 2세대에 가비지 컬렉션이 일어난다.
    그리고 0세대에서 제거되지 않는 개체는 1세대가 되고 , 1세대에서 제거되지 않는 개체는 2세대가 된다.
    그리고 2세대의 총 크기는 시스템이 허용하는 한 계속 커질 수 있다.

    아래는 가비지 컬렉션을 강제로 발생시키고, 세대 증가를 확인하는 코드이다.
    (테스트를 위해 GC.Collect를 호출한 것일 뿐이며, GC.Collect를 직접 호출하는 것은 권장되지 않는다.)

    string message = "Hello World!";
    
    // 0세대.
    Console.WriteLine($"Gen: {GC.GetGeneration(message)}");
    
    GC.Collect();
    
    // 1세대.
    Console.WriteLine($"Gen: {GC.GetGeneration(message)}");
    
    GC.Collect();
    
    // 2세대.
    Console.WriteLine($"Gen: {GC.GetGeneration(message)}");
    
    GC.Collect();
    
    // 2세대.
    Console.WriteLine($"Gen: {GC.GetGeneration(message)}");
    
    Console.ReadLine();

    즉, 낮은 세대 일수록 가비지 컬렉션이 자주 일어나고, 높은 세대 일수록 가비지 컬렉션이 적게 일어난다.

    압축

    가비지 컬렉션이 일어날 때 재밌는 점은 내부적으로 개체들의 메모리 주소가 변할 수 있다는 점이다.

    사용되지 않는 메모리가 관리 힙에서 빠진 후, 여전히 사용 중인 메모리는 빠진 공간을 메우도록 당겨지는데 이 과정에서 메모리 주소의 위치가 변할 수 있다.

    그리고 이 과정을 압축이라고 부른다.

    C#에서 포인터를 직접 다룰 때 fixed라는 키워드를 쓰는데, 이 키워드의 의미는 내가 직접 메모리를 다루고 있으니, 가비지 컬렉터가 멋대로 이 메모리 주소를 변경시키지 말아라! 하는 의미로 fixed 이름의 키워드였던 것이다.

    대용량 개체

    GC는 큰 개체(85000 바이트 이상)를 LOH라는 곳에 배치하여 별도로 관리한다.
    (압축 과정에서 큰 개체가 있을 경우, 비용이 매우 커질 수 있는 것도 이유 중 하나.)

    그리고 LOH에 위치한 개체들은 2세대 가비지 컬렉션 시에만 함께 가비지 컬렉션이 일어난다.
    (2세대는 0, 1, 2세대 그리고 LOH까지 가비지 컬렉션을 하니, 이러한 이유로 Full GC라고도 불린다.)

    이때, LOH에 있는 개체의 압축은 비용이 매우 많이 들기 때문에 GC는 LOH를 Sweep만 해둔다.

    1. 그리고 나중에 LOH에 할당 요청이 오면 이전에 Sweep 되었던 공간을 재사용 시도하고
    2. 재사용 할 수 없으면 OS에서 새 세그먼트(개체를 포함할 수 있는 새 메모리 덩어리) 를 획득하려고 시도하고
    3. 그것도 실패하면 2세대 GC를 발생시킨다.
    4. 그리고 살아있는 개체가 아무것도 없는 세그먼트는 OS에게 되돌려진다.

    기본은 위 동작과 같지만 GCSettings.LargeObjectHeapCompactionMode 설정으로 LOH도 압축하도록 지정 가능하고, 미래엔 LOH도 역시 자동적으로 압축 될 수 있다고 한다.

    LOH에 할당되는 개체는 2세대의 GC에 영향을 주기 때문에 성능 문제가 일으킬 여지가 있다.

    1. 큰 개체를 계속 할당한 후 내버려 두는 상황과,
    2. LOH에 위치하는 개체는 대게 긴 길이의 배열인데, 그 배열의 각 요소가 참조 타입을 많이 포함하는 상황을 경계하는 것이 좋다.

    2의 예는 긴 배열에 이진 트리를 구성하는 경우이며, 이 경우 가비지 컬렉터는 각 요소의 left와, right 참조를 들여봐야 한다.

    public class BadNode
    {
      public Data data;
      public Node left; // 참조가 있으므로 GC는 배열의 각 요소의 left와, right를 모두 봐야한다.
      public Node right;
    }
    
    BadNode[] badNodes = new BadNode[VeryVeryLongLength];
    
    public class GoodNode
    {
      public Data data;
      public int left;
      public int right;
    }
    
    GoodNode[] goodNodes = new GoodNode[VeryVeryLongLength];

    LOH에 대한 성능 분석은 Large object heap (LOH) on Windows | Microsoft Learn 이 링크를 참고

    참고

    1. Windows의 큰 개체 힙(LOH) | Microsoft Learn
    2. 알라딘: 시작하세요! C# 10 프로그래밍 (aladin.co.kr)
    3. 가비지 수집 기본 사항 | Microsoft Learn

     

     

    반응형
Designed by Tistory.