콘텐츠로 이동

.NET GC & 메모리 관리 심화

.NET GC는 자동으로 메모리를 관리하지만 잘못된 코드는 GC 압박(GC pressure)과 Stop-the-World 정지를 유발합니다. GC 동작 원리를 이해하면 불필요한 할당을 줄이고 지연 시간을 낮출 수 있습니다.


Generation 0 (Gen0) — 단기 객체 (최근 할당)
Generation 1 (Gen1) — Gen0 생존자 (중간)
Generation 2 (Gen2) — 장기 객체 (전체 수집)
Large Object Heap (LOH) — 85,000 바이트 이상 객체
  • Gen0 수집: 매우 빠름 (수 ms), 자주 발생
  • Gen2 수집: 느림 (수십 ms~초), Stop-the-World
  • LOH: 기본적으로 압축 없음 → 단편화 유발

using System;
using System.Runtime;
// GC 수집 횟수
Console.WriteLine($"Gen0: {GC.CollectionCount(0)}");
Console.WriteLine($"Gen1: {GC.CollectionCount(1)}");
Console.WriteLine($"Gen2: {GC.CollectionCount(2)}");
// 총 할당 메모리
Console.WriteLine($"총 할당: {GC.GetTotalAllocatedBytes(precise: false) / 1024 / 1024} MB");
// 힙 정보
var info = GC.GetGCMemoryInfo();
Console.WriteLine($"힙 크기: {info.HeapSizeBytes / 1024 / 1024} MB");
Console.WriteLine($"단편화: {info.FragmentedBytes / 1024} KB");

using System.Buffers;
// 나쁜 예: 매 요청마다 배열 할당
byte[] ProcessRequest(Stream stream)
{
byte[] buffer = new byte[4096]; // GC 압박
stream.Read(buffer, 0, buffer.Length);
return buffer;
}
// 좋은 예: ArrayPool 재사용
byte[] ProcessRequest(Stream stream)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
try
{
stream.Read(buffer, 0, buffer.Length);
return buffer.ToArray(); // 복사 후 풀에 반납
}
finally
{
ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
}
}

using System;
// 나쁜 예: 매번 새 문자열 할당
string[] parts = input.Split(','); // 힙 할당
// 좋은 예: Span으로 Zero-allocation 파싱
void ParseCsv(ReadOnlySpan<char> line)
{
while (!line.IsEmpty)
{
int comma = line.IndexOf(',');
ReadOnlySpan<char> field = comma < 0 ? line : line[..comma];
ProcessField(field); // 할당 없음
line = comma < 0 ? default : line[(comma + 1)..];
}
}

using Microsoft.Extensions.ObjectPool;
// 무거운 객체 풀링
public class ExpensiveObject
{
public byte[] Buffer { get; } = new byte[64 * 1024];
public void Reset() { /* 상태 초기화 */ }
}
public class ExpensiveObjectPolicy : IPooledObjectPolicy<ExpensiveObject>
{
public ExpensiveObject Create() => new ExpensiveObject();
public bool Return(ExpensiveObject obj)
{
obj.Reset();
return true;
}
}
// DI 등록
services.AddSingleton<ObjectPool<ExpensiveObject>>(sp =>
{
var provider = new DefaultObjectPoolProvider();
return provider.Create(new ExpensiveObjectPolicy());
});
// 사용
var obj = pool.Get();
try { /* 작업 */ }
finally { pool.Return(obj); }

// LOH 압축 활성화 (단발성 수집 전)
GCSettings.LargeObjectHeapCompactionMode =
GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
// 85KB 이상 배열은 ArrayPool 또는 NativeMemory 사용
using System.Runtime.InteropServices;
nint ptr = NativeMemory.Alloc(1024 * 1024); // LOH 우회
try { /* 사용 */ }
finally { NativeMemory.Free(ptr); }

7. GC 서버 모드 vs 워크스테이션 모드

섹션 제목: “7. GC 서버 모드 vs 워크스테이션 모드”
runtimeconfig.json
{
"configProperties": {
"System.GC.Server": true, // 서버 모드 (멀티코어 최적화)
"System.GC.Concurrent": true, // 백그라운드 GC
"System.GC.HeapHardLimit": 536870912 // 최대 힙 512MB
}
}
항목워크스테이션서버
GC 스레드1개코어당 1개
처리량낮음높음
지연 시간낮음높을 수 있음
적합 대상데스크톱 앱ASP.NET Core, 서비스

Terminal window
# dotnet-counters로 실시간 GC 모니터링
dotnet-counters monitor --process-id <PID> System.Runtime
# dotnet-trace로 GC 이벤트 수집
dotnet-trace collect --process-id <PID> --providers Microsoft-Windows-DotNETRuntime
# PerfView / dotnet-dump로 힙 스냅샷 분석

9. GCSettings — 지연 시간 모드 제어

섹션 제목: “9. GCSettings — 지연 시간 모드 제어”
using System.Runtime;
// 지연 시간 최소화가 중요한 구간에서 GC 억제
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
try
{
// 실시간 처리 구간 (예: 금융 거래, 게임 프레임)
ProcessCriticalSection();
}
finally
{
GCSettings.LatencyMode = GCLatencyMode.Interactive; // 원복
}
// 짧은 GC 억제 구간 (NoGCRegion)
bool entered = GC.TryStartNoGCRegion(16 * 1024 * 1024); // 16MB 보장
try
{
UltraLowLatencyWork();
}
finally
{
if (GC.IsInNoGCRegion())
GC.EndNoGCRegion();
}

// runtimeconfig.json (또는 앱 이름.runtimeconfig.json)
{
"configProperties": {
"System.GC.Server": true,
"System.GC.Concurrent": true,
"System.GC.HeapHardLimit": 1073741824,
"System.GC.HeapHardLimitPercent": 75,
"System.GC.HighMemoryPercent": 90
}
}
옵션설명
GC.Server서버 GC 활성화 (코어당 힙)
GC.Concurrent백그라운드 GC (앱 일시 정지 최소화)
GC.HeapHardLimit힙 최대 바이트 (컨테이너 환경 필수)
GC.HighMemoryPercent고메모리 임계값 (이 이상이면 적극 수집)

.NET GC 최적화의 핵심은 Gen2 수집과 LOH 할당을 줄이는 것입니다. 루프 내 단기 배열은 ArrayPool로, 문자열 파싱은 Span<T>으로, 무거운 객체는 ObjectPool로 교체하면 GC 압박이 크게 줄어듭니다. 실시간성이 요구되는 구간은 GCSettings.LatencyModeGC.TryStartNoGCRegion으로 GC를 일시 억제하고, 컨테이너 배포 시 HeapHardLimit으로 메모리 한도를 명시적으로 설정하세요.