
C#에서 가비지 컬렉터가 매번 전체 메모리를 샅샅이 뒤진다면,
그로 인한 성능 저하(Stop-the-World)를 무시할 수 없을 거예요.
그래서 .NET의 똑똑한 가비지 컬렉터는 훨씬 더 효율적인 전략을 사용합니다.
바로 세대별 가비지 컬렉션(Generational Garbage Collection)입니다.
이 전략이 어떻게 가비지 컬렉터를 더 빠르고 스마트하게 만드는지 알아보겠습니다.
컴퓨터 과학자들이 수많은 프로그램을 분석해 보니 다음과 같은 패턴을 발견했습니다.
"대부분의 객체는 생성된 직후에 금방 쓰레기가 된다."
"오래 살아남은 객체는 앞으로도 계속 살아남을 확률이 높다."
이를 '약한 세대 가설(Weak Generational Hypothesis)'이라고 부릅니다.
세대별 가비지 컬렉션은 객체의 수명에 따라 그룹으로 나누어 관리합니다.
즉, 객체를 세대별로 분류해서 청소 효율을 높이는 전략입니다.
.NET의 가비지 컬렉터는 객체를 총 3개의 세대(그룹)로 나누어 관리합니다.
new키워드로 객체를 생성하면 주로 0세대에 할당됩니다.1. 새 객체는 높은 확률로 0세대에 할당됩니다.
2. 0세대가 임계치에 도달하면 0세대 GC가 발생합니다.
3. 여기서 살아남은 객체들은 1세대로 승급합니다.
4. 1세대가 임계치에 도달하면 1세대 GC가 발생합니다. (0세대와 1세대 함께 청소)
5. 1세대 GC에서 살아남은 객체들은 2세대로 승급합니다.
6. 이 과정이 반복되다가 2세대마저 임계치에 도달하면, 전체 메모리를 대청소합니다.
이처럼 가비지 컬렉터는 가장 쓰레기가 많을 것으로 예상되는 0세대를 집중적으로
청소함으로써 시스템 멈춤(Stop-the-World) 시간을 최소화하고 효율은 극대화합니다.
가비지 컬렉터의 동작은 우리가 직접 제어하기 어렵지만,
GC클래스를 통해 객체가 어느 세대에 속해 있는지 확인해 볼 수는 있습니다.
[코드]
class Program
{
static void Main()
{
Console.WriteLine("프로그램 시작!");
// longLivedObject는 Main 메서드가 끝날 때까지 살아있을 확률이 높습니다.
var longLivedObject = new object();
Console.WriteLine($"초기 세대: Gen {GC.GetGeneration(longLivedObject)}");
// GC.Collect()를 강제로 여러 번 발생시켜 객체의 승급을 관찰해봅니다.
// 주의! GC.Collect()는 성능에 영향을 주므로 학습 및 디버깅 목적으로만 사용하세요.
GC.Collect(0); // Gen 0 수집
Console.WriteLine($"Gen 0 수집 후: Gen {GC.GetGeneration(longLivedObject)}");
GC.Collect(1); // Gen 1 수집
Console.WriteLine($"Gen 1 수집 후: Gen {GC.GetGeneration(longLivedObject)}");
GC.Collect(2); // Gen 2 수집 (Full GC)
Console.WriteLine($"Gen 2 수집 후: Gen {GC.GetGeneration(longLivedObject)}");
// 이 객체를 계속 참조하고 있으므로 프로그램 종료 시점까지 살아남습니다.
GC.KeepAlive(longLivedObject);
}
}
[실행 결과]
프로그램 시작!
초기 세대: Gen 0
Gen 0 수집 후: Gen 1
Gen 1 수집 후: Gen 2
Gen 2 수집 후: Gen 2
가비지 컬렉터는 쓰레기가 나올 확률이 가장 높은 0세대 가비지 컬렉션을
집중적으로 관리함으로써 프로그램의 성능을 최대한 보장해 줍니다.
| 세대 | 역할 | GC 빈도 | 비유 |
|---|---|---|---|
| Gen 0 | 새로 생성된 대부분의 객체 | 매우 높음 | 받은 문서함 |
| Gen 1 | Gen 0에서 살아남은 객체 | 중간 | 처리할 일 목록 |
| Gen 2 | Gen 1에서 살아남은 객체 | 매우 낮음 | 장기 보관 캐비닛 |