왜 StringBuilder의 버퍼 크기는 8000으로 제한되어있을까? (LargeObjectHeap의 등장)

마수리·2024년 8월 25일
0

StringBuilder

목록 보기
5/5
post-thumbnail

안녕하세요 마수리입니다. 오늘은 StringBuilder의 소스코드에 쓰여져있던 주석을 읽으면서 새로운 개념을 하나 공부하게 되어 공유하게 되었습니다. StringBuilder에는 특정한 크기의 버퍼가 있는데 버퍼가 꽉차면 새로운 버퍼를 만들게 되어있습니다. 이때 버퍼의 크기를 정해야하는데 버퍼의 크기는 아래와 같이 정해지고 있습니다.

// Compute the length of the new block we need 
// We make the new chunk at least big enough for the current need (minBlockCharCount)
// But also as big as the current length (thus doubling capacity), up to a maximum
// (so we stay in the small object heap, and never allocate really big chunks even if
// the string gets really big. 

// minBlockCharCount: 부족한 버퍼의 크기
// Length: 모든 버퍼 크기의 합
// MaxChunkSize: 버퍼의 최대 값.
internal const int MaxChunkSize = 8000;
int newBlockLength = Math.Max(minBlockCharCount, Math.Min(Length, MaxChunkSize)); 

이 글의 시작점은 바로 이 코드였고 코드에 달려있는 주석이였습니다.

코드를 보시면 Math.Min()이라는 함수를 사용해서 newBlockLength가 절대로 8000을 넘을 수 없도록 하고 있습니다. 그 이유는 주석에 설명 되어있습니다. 주석의 의미를 한글로 풀면 아래와 같습니다.

필요한 새 버퍼의 길이를 계산합니다. 새 버퍼를 최소한 현재 필요한 만큼(minBlockCharCount) 크게 만들되, 현재 모든 버퍼의 총 길이만큼(따라서 용량을 두 배로) 최대로 만듭니다(따라서 문자열이 정말 커지더라도 small object heap을 유지하고 정말 큰 버퍼는 할당하지 않습니다.) 문자열이 정말 커지더라도 큰 버퍼는 할당하지 않습니다.

이 코드는 버퍼가 small object heap이라는 곳에 할당 되도록 큰 버퍼는 할당하지 않는다고 설명하고 있습니다. 그럼 large object heap도 있을까요? 네! 있습니다! 😎 다만, large object heap을 알기위해 다른 개념이 먼저 등장해야 합니다.

.NET의 가비지 컬렉팅

LOH를 이해하기 위해선 간단하게 .NET의 가비지 컬렉팅 방식을 이해해야합니다.

이 글에서 다루긴 너무 큰 내용이라 큼지막하게 개념만 훑고 가겠습니다.

  • .NETGC는 3개의 세대라는 것을 기준으로 좀 더 오래 살아남을 데이터와 그렇지 않을 데이터를 구분합니다. 예를들어 0세대의 데이터는 2세대의 데이터보다 더 자주 수집됩니다. 한번 수집이 이루어질 때 사용 중이라 수집되지 못한 데이터는 다음 세대로 승격 되어 좀 더 오래 메모리 공간에 살아있게 됩니다.

  • 2세대의 가비지를 수집한다는 것은 0세대1세대도 모두 수집한다는 것을 의미합니다. 이 의미는 모든 세대GC가 트리거 된다는 소리고 결국 2세대 가비지 수집은 전체GC 라고 불리기도 합니다. 전체GC의 비용은 큽니다.

LOH(Large Object Heap)의 등장

LOH는 위와 같은 GC의 특징으로부터 안정적인 성능을 보장하기 위해 새로운 세대로 지칭되는 물리적으로 새로운 3세대 공간입니다. 이 공간은 2세대 GC가 발생하면 같이 수집됩니다.

  • GC이후 메모리의 파편화를 막기위해 압축 과정이 있는데 이때 큰 오브젝트의 복사비용이 크게 발생합니다.
    • 그렇기 때문에 LOHGC후 메모리 공간을 압축하지 않습니다.
    • 옵션으로 압축하도록 변경도 가능하긴 합니다.
  • 아주 큰 객체를 할당하기 위해 물리적으로 새로운 공간OS에 요청해 할당받고 이러한 공간을 세그먼트라고 합니다.
    • 더 많은 LOH를 위한 공간이 필요하면 새로운 세그먼트를 요청할 수 있습니다.
    • 이 말은 세그먼트라는 공간은 여러개가 생길 수 있습니다.
    • 계속적으로 LOH공간이 필요하면 계속해서 필요한 공간을 할당 받기 위해 전체GC를 하지 않고 세그먼트를 할당받아 그곳에 적재합니다
    • 세그먼트내의 파편화가 심해 새로운 세그먼트를 계속 요청하게 되다보면 OS에서 세그먼트로 내줄 수 있는 메모리 공간이 부족해지고 이땐 어쩔 수 없이 전체GC를 하게 됩니다.

결국 아주 큰 메모리 공간을 필요로하는 메모리들은 물리적으로 새로운 공간인 세그먼트라는 것으로 관리하고 GC를 할 때 메모리 압축을 하지 않는 등의 최적화를 위해서 설계되었다.
전체GC 때만 LOHGC를 진행하고 할당할 메모리가 부족할 경우 바로 GC를 진행해 할당 가능한 메모리를 만드려고 하지 않고 OS에 요청해 아예 새로운 LOH의 전용 메모리 공간인 세그먼트를 할당 받는 등 최대한 GC의 빈도를 더 적게 가져갑니다.

이러한 GC의 특징으로인해 물리적으로 새로운 3세대 공간세그먼트라는 개념을 두어 따로 관리한 것입니다.

마무리

정리를 해보겠습니다. 오늘의 주제는 왜 StringBuilder에서는 특정 크기 이상으로 메모리 할당을 제한하려 했는가에 대한 대답은 LOH의 개념 때문이였고 StringBuilder의 특징상 버퍼로 잡았던 모든 공간은 사용자가 ToString() 함수를 호출해서 최종적으로 string객체를 뽑아 냈을 때 모두 사라져도 되는 메모리 공간이 되므로 이러한 임시 버퍼의 공간을 LOH에 할당하고 싶지 않았던 것입니다. LOH를 삭제하기 위해선 전체GC가 트리거 되어야하고 이것은 성능상 문제가 되기 때문입니다. 그렇다고 전체GC를 하지 않기엔 StringBuilder에서 사용했던 모든 버퍼는 앞으로 사용되지 않는 공간이기 때문에 삭제 되지 않고 있다면 메모리릭이 발생한 것이 될 것입니다.

profile
.NET 개발자 마수리입니다 🖐

0개의 댓글