.NET GC & 메모리 관리 심화
.NET GC는 자동으로 메모리를 관리하지만 잘못된 코드는 GC 압박(GC pressure)과 Stop-the-World 정지를 유발합니다. GC 동작 원리를 이해하면 불필요한 할당을 줄이고 지연 시간을 낮출 수 있습니다.
1. 세대별 GC 구조
섹션 제목: “1. 세대별 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: 기본적으로 압축 없음 → 단편화 유발
2. GC 통계 확인
섹션 제목: “2. GC 통계 확인”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");3. ArrayPool — 배열 재사용
섹션 제목: “3. ArrayPool — 배열 재사용”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); }}4. Span으로 할당 없는 슬라이싱
섹션 제목: “4. Span으로 할당 없는 슬라이싱”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)..]; }}5. 객체 풀링 — ObjectPool
섹션 제목: “5. 객체 풀링 — ObjectPool”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); }6. LOH 단편화 방지
섹션 제목: “6. LOH 단편화 방지”// 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 워크스테이션 모드”{ "configProperties": { "System.GC.Server": true, // 서버 모드 (멀티코어 최적화) "System.GC.Concurrent": true, // 백그라운드 GC "System.GC.HeapHardLimit": 536870912 // 최대 힙 512MB }}| 항목 | 워크스테이션 | 서버 |
|---|---|---|
| GC 스레드 | 1개 | 코어당 1개 |
| 처리량 | 낮음 | 높음 |
| 지연 시간 | 낮음 | 높을 수 있음 |
| 적합 대상 | 데스크톱 앱 | ASP.NET Core, 서비스 |
8. 진단 도구
섹션 제목: “8. 진단 도구”# 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로 힙 스냅샷 분석.NET GC 최적화의 핵심은 Gen2 수집과 LOH 할당을 줄이는 것입니다. 루프 내 단기 배열은 ArrayPool로, 문자열 파싱은 Span<T>으로, 무거운 객체는 ObjectPool로 교체하면 GC 압박이 크게 줄어듭니다. GC.CollectionCount(2) 증가 속도가 빠르면 메모리 프로파일러로 원인을 찾으세요.