C# .NET의 GC와 LOH/SOH에 대해 알아보자
C#은 메모리 관리를 자동으로 처리하는 Managed 언어로, 그 핵심 기능 중 하나가 Garbage Collection 다. 개발자가 메모리 할당과 해제를 신경 쓸 필요 없이, 프로그램이 실행되는 동안 메모리를 자동으로 관리해준다. 이를 통해 메모리 누수나 비효율적인 메모리 사용을 방지할 수 있다. 이번 글에서는 .NET에서 GC가 어떻게 동작하는지, 그리고 Large Object Heap(LOH) 와 Small Object Heap(SOH) 에 대해 알아보려고 한다.
Garbage Collection이란?
Garbage Collection 은 .NET에서 더 이상 사용되지 않는 객체를 자동으로 추적해서 메모리에서 제거하는 기능이다. 메모리 관리가 제대로 이루어지지 않으면 프로그램의 성능이 떨어지거나 메모리 누수가 발생할 수 있다. GC는 이런 문제를 해결하기 위해 설계되었고, 이를 통해 개발자는 메모리 관리를 자동으로 맡기고 성능 최적화에 집중할 수 있다.
GC의 역할은 크게 세 가지다:
- 메모리 할당: 새로운 객체가 메모리에 할당된다.
- 메모리 해제: 더 이상 사용되지 않는 객체를 찾아 메모리에서 해제한다.
- 메모리 압축: 가비지가 제거된 후, 힙 공간을 최적화해 새로운 객체들이 효율적으로 할당될 수 있도록 한다.
.NET의 GC 제너레이션(Generation)
. NET에서는 GC가 객체를 추적하고 수집하는 방식을 Generation 으로 나눈다. 제너레이션은 객체의 수명이 길어질수록 GC가 더 효율적으로 동작하도록 돕는다. .NET에서는 객체의 수명에 따라 GC를 3개의 제너레이션으로 구분한다.
1. Generation 0
제너레이션 0은 가장 새로운 객체들이 할당되는 영역이다. 보통 새로 생성된 객체들이 제너레이션 0에 할당된다. 제너레이션 0에 있는 객체들은 수명이 짧고, 자주 GC가 실행되어 이들을 회수한다. 만약 객체가 제너레이션 0에서 살아남으면 제너레이션 1로 승격된다.
GC가 제너레이션 0을 수집할 때 살아남은 객체만 제너레이션 1로 승격된다. 이 과정은 비교적 자주 발생한다.
2. Generation 1
제너레이션 1은 제너레이션 0에서 승격된 객체들이 있는 영역이다. 제너레이션 1에 있는 객체들은 제너레이션 0보다 상대적으로 더 오래 살아남은 객체들이다. GC는 이들을 자주 수집하지 않는다. 제너레이션 0에 비해 객체가 더 오래 살아남았기 때문에, GC는 제너레이션 1에 대해 덜 자주 수집을 한다.
제너레이션 1에서 살아남은 객체들은 제너레이션 2로 승격된다. 이 과정은 비교적 적은 빈도로 일어난다.
3. Generation 2
제너레이션 2는 가장 오래 살아남은 객체들이 모인 영역이다. 이 객체들은 애플리케이션 전체 실행 동안 계속 살아남을 가능성이 높고, GC가 이들을 수집하는 빈도는 매우 낮다. 제너레이션 2에 있는 객체들은 다른 제너레이션에 비해 수명이 길고, 대체로 대규모 데이터 구조나 상태를 가진 객체들이다.
제너레이션 2의 수집은 Full GC 라고 불리며, 이는 모든 객체를 검사하고 수집하는 작업이다. Full GC는 시간이 오래 걸리고 성능에 큰 영향을 미칠 수 있기 때문에 자주 일어나지 않도록 해야 한다.
LOH와 SOH
C#에서 GC는 Small Object Heap 와 Large Object Heap 라는 두 가지 중요한 메모리 영역을 관리한다. 이 두 영역은 GC가 객체를 관리하는 방식에 있어 중요한 차이를 만든다.
1. Small Object Heap
Small Object Heap 는 제너레이션 0과 제너레이션 1에 할당되는 작은 객체들이 저장되는 메모리 영역이다. 일반적으로 85,000바이트 이하의 객체들이 SOH에 저장된다.
- 빠른 수집 주기: SOH의 객체들은 자주 메모리에서 수집된다. GC가 자주 실행되며, 제너레이션 0과 1의 객체들은 자주 회수되고 승격된다.
- 효율적 메모리 관리: SOH에 저장된 객체들은 상대적으로 작은 크기이므로 메모리 관리가 효율적이다.
- GC 최적화: 작은 객체들은 자주 GC가 수집되고, 압축도 빠르게 이루어지기 때문에 성능을 최적화하는 데 유리하다.
2. Large Object Heap
Large Object Heap 는 제너레이션 2에 속하는 큰 객체들이 저장되는 메모리 영역이다. LOH에는 85,000바이트 이상의 크기를 가진 객체들이 할당된다.
- Full GC: LOH에 있는 객체들은 Full GC에서만 수집될 수 있다. Full GC는 제너레이션 0, 1, 2 모두를 검사하고 수집하는 작업으로 시간이 많이 걸리며 성능에 큰 영향을 미칠 수 있다.
- 압축 불가: LOH는 큰 객체들이 저장되기 때문에 압축이 어렵다. 이로 인해 메모리에서 파편화가 발생할 수 있으며, 성능 저하를 유발할 수 있다.
LOH와 SOH의 차이점
항목 | Small Object Heap (SOH) | Large Object Heap (LOH) |
---|---|---|
객체 크기 | 85,000바이트 이하 | 85,000바이트 이상 |
GC 제너레이션 | 제너레이션 0, 1 | 제너레이션 2 |
GC 주기 | 자주 발생 | 드물게 발생 (Full GC) |
수집 방식 | 빠르고 자주 수집 (Generation 0, 1) | Full GC에서만 수집 |
메모리 압축 | 압축이 빠르게 이루어짐 | 압축이 어려움 (메모리 파편화 가능) |
파편화 리스크 | 낮음 | 높음 |
메모리 관리 효율성 | 매우 효율적 | 상대적으로 비효율적 (Full GC에서만 관리) |
LOH의 성능 문제와 최적화 방법
LOH에 할당된 큰 객체들은 제너레이션 2에서 관리되므로, Full GC가 발생해야만 수집된다. Full GC는 시간이 오래 걸리고 성능에 큰 영향을 미칠 수 있다. LOH를 잘 관리하는 방법은 다음과 같다:
- 객체 크기 분할: 너무 큰 객체는 여러 개의 작은 객체로 나누는 것이 좋다. 이를 통해 SOH에 할당되도록 유도할 수 있으며, GC 성능을 향상시킬 수 있다.
- 배열 최적화: 큰 배열을 자주 사용한다면, 배열 크기를 최소화하거나 메모리에서 자주 해제할 수 있는 방식으로 배열을 처리해야 한다.
- MemoryPool 사용: ArrayPool
나 MemoryPool 와 같은 객체 풀을 사용하여 큰 객체들을 재사용할 수 있다. 이는 GC의 부담을 줄이고 메모리 관리 효율성을 높인다. - GC.Collect() 사용: GC.Collect() 메서드를 사용하여 강제로 GC를 호출할 수 있지만, LOH에서는 자주 호출하지 않는 것이 좋다. LOH는 Full GC가 필요하기 때문에, 강제 GC 호출은 성능 저하를 초래할 수 있다.
- 큰 객체 사용 최소화: 가능한 경우 LOH에 큰 객체를 할당하지 않는 것이 가장 좋다. 큰 객체를 사용하는 경우, 시스템의 메모리 상태나 GC의 영향을 고려해야 한다.
결론
C#에서 GC 는 자동으로 메모리를 관리하는 강력한 시스템이다. 하지만 SOH 와 Large Object Heap 라는 두 가지 메모리 영역을 이해하는 것이 성능 최적화에 매우 중요하다. SOH는 작은 객체를 관리하고 빠른 GC가 가능하지만, LOH는 큰 객체들을 관리하고 상대적으로 자주 GC가 발생하지 않는다. Large Object Heap의 관리 방법에 대한 최적화를 통해 C# 애플리케이션의 메모리 사용을 효율적으로 개선할 수 있다.
따라서, C# 개발자는 GC 제너레이션과 LOH/SOH의 차이를 이해하고, 이를 바탕으로 최적화 전략을 적용하여 성능을 극대화할 수 있다.