메모리 파편화와 압축

메모리 파편화는 메모리 공간이 불연속적으로 나뉘어져 필요한 크기의 연속된 메모리를 할당하지 못하는 현상을 말합니다. 이는 애플리케이션의 성능 저하와 메모리 사용량 증가를 유발할 수 있습니다. 이번 글에서는 메모리 파편화의 원인과 영향, 그리고 이를 해결하기 위한 압축(compaction)과 GC(Garbage Collection)의 역할에 대해 알아보겠습니다.

메모리 파편화의 원인과 영향

메모리 파편화란?

메모리 파편화는 두 가지로 분류됩니다:

  • 외부 파편화(External Fragmentation): 사용 가능한 메모리의 총량은 충분하지만, 연속된 큰 메모리 블록이 없어 메모리 할당이 실패하는 현상입니다.
  • 내부 파편화(Internal Fragmentation): 할당된 메모리 블록 내에서 실제로 사용되지 않는 공간이 발생하는 현상입니다.

메모리 파편화의 원인

  • 빈번한 메모리 할당과 해제: 객체의 생성과 소멸이 반복되면서 메모리 공간에 불연속적인 빈 공간이 생깁니다.
  • 다양한 크기의 객체 할당: 서로 다른 크기의 객체를 할당하고 해제할 때 메모리 블록의 크기가 일치하지 않아 파편화가 발생합니다.
  • 큰 객체의 빈번한 할당(LOH 문제): Large Object Heap(LOH)에서 큰 객체의 할당과 해제가 빈번하면 파편화가 심해집니다.

메모리 파편화의 영향

  • 메모리 사용량 증가: 파편화로 인해 사용되지 않는 메모리 공간이 증가하여 전체 메모리 사용량이 늘어납니다.
  • 성능 저하: 메모리 할당 시간이 증가하고, GC의 수집 시간이 길어져 애플리케이션의 성능이 저하됩니다.
  • OutOfMemoryException 발생: 충분한 메모리가 있음에도 불구하고 연속된 메모리 블록을 확보하지 못해 예외가 발생할 수 있습니다.

메모리 압축과 GC의 역할

메모리 압축(Compaction)이란?

메모리 압축은 메모리에 흩어져 있는 객체들을 한쪽으로 모아 연속된 메모리 공간을 만드는 과정입니다. 이를 통해 파편화를 줄이고 메모리 사용 효율을 높일 수 있습니다.

GC에서의 메모리 압축

GC는 가비지 컬렉션 과정에서 메모리 압축을 수행하여 파편화를 해결합니다. 세대별 가비지 컬렉션에서 Gen 0과 Gen 1에서는 기본적으로 압축이 이루어집니다. 예시:

// 객체 생성 및 해제 예시
for (int i = 0; i < 1000; i++)
{
    byte[] data = new byte[1024]; // 1KB 객체
    // 데이터 처리
    data = null;
}
// GC 수집 후 메모리 압축으로 파편화 해소

Large Object Heap(LOH)에서의 압축

LOH는 기본적으로 메모리 압축을 수행하지 않으므로 파편화가 누적될 수 있습니다. 그러나 .NET Framework 4.5.1 이상에서는 LOH 압축을 활성화할 수 있습니다. LOH 압축 활성화 방법:

// LOH 압축 실행
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();

또는 구성 파일에서 설정:

<configuration>
  <runtime>
    <gcAllowVeryLargeObjects enabled="true"/>
    <GCLargeObjectHeapCompactionMode enabled="true"/>
  </runtime>
</configuration>

파편화 해결을 위한 전략과 설정

객체 크기 최적화

  • 큰 객체의 할당 최소화: 가능한 한 큰 객체의 할당을 피하고, 필요한 경우 작은 객체로 분할합니다.
  • 데이터 구조 개선: 메모리 효율적인 데이터 구조를 사용하여 메모리 사용량을 줄입니다. 예시:
// 큰 배열 대신 리스트 사용
List<byte[]> dataChunks = new List<byte[]>();
for (int i = 0; i < 100; i++)
{
    byte[] chunk = new byte[1024]; // 1KB 청크
    dataChunks.Add(chunk);
}

메모리 풀링(Memory Pooling) 활용

  • ArrayPool<T> 사용: 배열을 재사용하여 메모리 할당과 파편화를 줄입니다. 예시:
// ArrayPool 사용 예시
var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(1024);
try
{
    // 버퍼 사용
}
finally
{
    pool.Return(buffer);
}

세대별 가비지 컬렉션 활용

  • 짧은 수명의 객체 관리: 짧은 수명의 객체는 Gen 0에서 수집되므로, 불필요한 객체 생성을 줄여 파편화를 완화합니다.
  • 긴 수명의 객체 관리: 긴 수명의 객체는 Gen 2로 승격되므로, 메모리 누수가 없도록 주의합니다.

GC 설정 조정

  • Server GC 사용: 멀티코어 환경에서 Server GC를 활성화하여 가비지 컬렉션 성능을 향상시킵니다.
  • 백그라운드 GC 활성화: 백그라운드에서 가비지 컬렉션을 수행하여 애플리케이션 중단 시간을 줄입니다. 구성 파일 설정 예시:
<configuration>
  <runtime>
    <gcServer enabled="true"/>
    <gcConcurrent enabled="true"/>
  </runtime>
</configuration>

메모리 파편화 진단 방법

프로파일링 도구 사용

  • Visual Studio Diagnostics Tools: 메모리 사용량과 힙 상태를 시각적으로 확인할 수 있습니다.
  • dotMemory: 메모리 할당과 파편화를 상세하게 분석할 수 있습니다.

힙 덤프 분석

  • Debug Diagnostic Tool: 메모리 덤프를 생성하고 분석하여 파편화 여부를 확인합니다.
  • WinDbg와 SOS.dll: 힙의 상태를 상세히 조사할 수 있습니다.

메모리 압축 시의 고려 사항

성능 영향

메모리 압축은 메모리 복사 작업을 수반하므로 CPU 사용량이 증가할 수 있습니다. 따라서 압축이 필요한 시점에만 수행하는 것이 좋습니다.

LOH 압축 시기 조절

LOH 압축은 필요할 때 명시적으로 수행하도록 설정할 수 있습니다. 애플리케이션의 메모리 사용 패턴에 따라 압축 시점을 조절합니다.

결론

메모리 파편화는 애플리케이션의 성능과 안정성에 부정적인 영향을 미칠 수 있습니다. GC의 메모리 압축 기능과 적절한 메모리 관리 전략을 활용하여 파편화를 해결할 수 있습니다. 객체 크기 최적화, 메모리 풀링, GC 설정 조정 등을 통해 메모리 사용 효율을 높이고 성능을 향상시킬 수 있습니다.