← 문제로

18. GCHandle/fixed 핀닝과 GC 압축(compaction)의 상호작용

난이도 상
내 리뷰 · C#
해설 · C#

해설 — GCHandle/fixed 핀닝과 GC 압축(compaction)의 상호작용

난이도: 상

요약

.NET GC 는 수집 시 살아있는 객체를 이동(compaction) 시켜 단편화를 줄인다. 관리 객체를 네이티브 포인터로 다루는 동안 GC 가 그 객체를 옮기면 포인터가 대롱거리므로, fixed(짧게)·GCHandle(Pinned)(오래)로 이동을 금지(pin) 한다. 핀 고정은 포인터를 안정시키지만 압축을 방해해 단편화를 유발한다(특히 gen0/LOH). 관리 배열과 포인터는 같은 저장소라 한쪽 쓰기가 양쪽에 보인다.

예측 출력

True
10 40
True
99

핵심 개념 · 단계별

줄1: fixed 안의 포인터 안정성 — True

  • fixed (byte* p = buf)buf블록 동안 핀 고정하고, 배열 첫 원소 주소를 p 로 준다. 블록 내에서 GC 가 buf 를 이동할 수 없으므로 p 는 유효·불변. q = pp == q = True.
  • 핵심 제약: 이 안정성은 fixed 블록 안에서만 보장. 블록을 벗어나면 핀이 풀려 다음 GC 가 buf 를 이동할 수 있고, 저장해 둔 포인터는 무효(대롱). 그래서 fixed 밖으로 포인터를 새어 나가게 하면 안 된다.

줄2: 포인터 쓰기가 managed 뷰에 반영 — 10 40

  • p[0]=10; p[3]=40;buf동일한 힙 저장소를 직접 쓴 것. 관리 인덱서 buf[0], buf[3] 로 읽으면 같은 바이트라 10 40. (별도 복사가 아니라 같은 메모리의 두 뷰.)

줄3: GCHandle(Pinned) 은 GC 후에도 주소 불변 — True

  • GCHandle.Alloc(buf, Pinned)fixed 보다 오래(핸들을 Free() 할 때까지) buf 를 고정한다. AddrOfPinnedObject() 는 고정된 첫 원소 주소.
  • GC.Collect() 로 강제 수집해도 핀된 객체는 이동 대상에서 제외되므로 addr1 == addr2 = True. 이 안정성 덕에 비동기 소켓 I/O·P/Invoke 가 진행되는 동안 커널/네이티브가 그 버퍼를 안전하게 참조한다.

줄4: 핸들 통해 쓴 값도 managed 뷰에 — 99

  • Marshal.WriteByte(addr1, 1, 99)buf[1] 저장소에 99 를 씀 → 관리 뷰 buf[1] = 99. (핸들 Free() 는 핀만 해제하지 데이터를 지우지 않는다.)

흔한 오해·함정

  • 핀은 공짜가 아니다: 압축을 막으므로 gen0/gen1 에 핀된 객체가 흩어지면 단편화로 힙이 커지고 수집 효율이 떨어진다. 오래·다수 핀은 특히 나쁘다.
  • 수명 규칙: fixed 는 스코프 기반(짧게), 비동기/장기 interop 은 GCHandle(Pinned) 또는 POH(Pinned Object Heap, .NET 5+) 에 버퍼를 두어 단편화를 국소화. 소켓 라이브러리는 흔히 큰 버퍼를 미리 핀해 재사용(핀 횟수·단편화 최소화).
  • GCHandle.Free() 누락 = 영구 핀 + 핸들 누수: 반드시 try/finally(위 코드) 또는 SafeHandle. (csharp_internals/problem8 IDisposable/Dispose 패턴과 같은 결정적 해제 원칙.)
  • fixed 밖으로 포인터 반출 금지: 블록을 벗어나면 이동 가능 → 대롱 포인터. Span<T>(스택 전용, csharp_internals/problem5)나 Memory<T>.Pin() 이 더 안전한 현대적 대안.
  • stackalloc/Span 은 애초에 이동하지 않는 스택 메모리라 핀이 불필요 — 짧은 버퍼엔 이쪽이 낫다.

면접 포인트

  • GC 압축이 왜 포인터를 위협하고, fixed(스코프)와 GCHandle(Pinned)(장기)의 차이·용도.
  • 핀닝이 단편화를 만드는 메커니즘과 완화책(버퍼 재사용·POH·Span/Memory.Pin).
  • 관리 배열과 네이티브 포인터가 같은 저장소를 공유하는 점, 그리고 fixed 밖으로 포인터를 내보내면 안 되는 이유.