메모리 단편화와 GC, non-GC

동적할당에 대해 어느 정도 안다고 가정한다.

동적할당은 생각보다 고려할게 많다. 반복적으로 할당/해제되는 것에 대해서 메모리 단편화를 막기 위해 실제 메모리 배치를 compact하게 유지할 필요가 있기 때문이다.




메모리 단편화 (Memory Fragmentation)

메모리 파편화라고도 부른다.
메모리 단편화에는 크게 2가지가 존재한다. 보통 문제가 되는건 외부 메모리 단편화다.


A. 내부 메모리 단편화(External Memory Fragmentation)

내부 메모리 단편화는 비교적 단순한 문제다. 그냥 정해진 블록 크기보다 작은 데이터가 할당되어서 공간이 낭비되는 현상을 가리킨다.

https://velog.io/@hanhs4544/%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%8B%A8%ED%8E%B8%ED%99%94Memory-Fragmentation


B. 외부 메모리 단편화(External Memory Fragmentation)

외부 메모리 단편화는 그것과 다르게, 잦은 할당/해제로 인해서 중간중간 성긴 구멍이 생기고, 그것으로 인해서 실제 여유공간을 전체적으로 활용할 수 없는 문제를 말한다.

.png?type=w800) https://velog.io/@hanhs4544/%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%8B%A8%ED%8E%B8%ED%99%94Memory-Fragmentation
보통은 이게 문제가 될 일이 더 잦다.




OS의 메모리 관리 기법

OS는 메모리를 효율적으로 관리하고 단편화를 줄이기 위해 몇가지 방법을 사용한다.
페이징과 세그먼트다.


**A. 페이징 (Paging) **

실제 메모리와 별개로 페이지(Page)라고 하는 가상 메모리 블럭들을 정의해서, 그걸 통해서 실제 메모리에 접근하는 방법이다. 대부분의 OS가 이 방법론을 사용해서 메모리를 관리한다.

.png?type=w800) https://velog.io/@hanhs4544/%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%8B%A8%ED%8E%B8%ED%99%94Memory-Fragmentation
이 방법을 사용하면 실제 메모리가 연속적이지 않아도 되기 때문에 외부 단편화는 해결된다.
대신 정해진 페이지 크기를 통째로 할당해서 쓰기 때문에 내부 단편화가 발생한다.

일반적인 OS들의 페이지 기본 크기는 4K에서부터 시작한다.


B. 세그먼트 (Segment)

페이지가 고정된 크기의 블럭을 할당해서 사용하는 방법이라면, 세그먼트는 가변적인 크기의 블럭을 할당해서 연결하는 방법이다.

.png?type=w800) https://velog.io/@hanhs4544/%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%8B%A8%ED%8E%B8%ED%99%94Memory-Fragmentation
세그먼트는 연속적으로 할당된다.
이건 미리 할당해놓고 연결하는 그런게 아니라서 외부 단편화가 발생한다.
하지만 필요한 크기만큼만 할당하기 때문에 내부 단편화는 없다.

이 기법은 16비트에서 시작해서 32비트까지만 주로 사용되었고, 64비트 플랫폼부터는 버려지고 있는 추세다.

32비트 플랫폼에서는 페이징과 세그먼트를 결합해서 사용했는데, 여기서 세그먼트는 메모리 할당을 위해서가 아니라, 메모리 보호를 위해서 사용되었다.
64비트 플랫폼에서도 부분적으로 보호를 위한 매커니즘에서 사용되고 있다.


C. 근황

정리하자면 32비트 플랫폼에서는 페이징과 세그먼트를 결합해서 사용했지만, 64비트 플랫폼에서는 거의 페이징 기법만을 사용해서 메모리 할당을 관리하고 있다는 것이다.




GC와 non-GC 언어

Java, V8, C# 같은 대부분의 GC 언어들은 메모리 단편화에 대한 문제를 해결하기 위해 메모리 압축(Compaction)이란 것을 실행한다.
꽤 심각한 병목이 생길 수 있음에도 단편화 하나만을 막기 위해 꽤 부담스러운 작업을 주기적으로 행하는 것이다.

근데 C/C++/Rust 같은 non-GC 언어들은 그런 것에 대한 고민이 별로 없는 것처럼 보인다. 왜일까?




non-GC 언어 (C/C++, RUST)

사실 C/C++ 같은 non-GC 언어들도 잠재적으로 메모리 단편화에 대한 문제가 존재할 수 있다.
하지만 그 대응 방법이 GC 언어와 다르고, 사용자가 직접 그 할당을 제어할 수 있다는 것이 다를 뿐이다.

GC 언어만이 자동화된 Memory Compaction을 수행하는 이유는, GC 언어에서만 메모리 단편화가 발생해서가 아니다. 그냥 자동으로 메모리 할당을 전부 책임진다는 결정 아래에서 이어진 것일 뿐이다.

A. Memory Compaction?
GC 언어들은 객체가 할당될때마다 그 소유권이 항상 GC 매니저에 맡겨진다. 언제든 객체가 어디있는지 알 수 있다는 것이다.
반면 C/C++ 같은 unmanaged 언어들은 그런 책임이나 추적이 보장되지 않는다. 애초에 불가능한 것이다.

B. 32비트와 64비트
32비트 플랫폼에서는 실제로 장기 실행형 C/C++ 앱에 외부 단편화로 인한 문제가 꽤 발생할 수 있었다.
예를 들어, 32비트 플랫폼에서 2GB 짜리 메모리만 다뤄도 문제가 바로 불거질 수 있다.
4GB 정도가 상한이기 때문이다.

하지만 64비트 플랫폼에서는 주소 공간이 너무 넓어서, 메모리 매핑을 거의 끝없이 만들 수 있기 때문에 외부 단편화로 인한 문제 발생률이 매우 드물어졌다.

**C. Memory Pool **
non-GC 언어에서도 할당 최적화를 위해서 Memory Pool을 미리 고정으로 할당해놓고 돌려쓰기도 한다.
Memory Arena라고도 부른다.

D. Custom Allocator
C/C++, Rust 같은 언어들은 OS에서 제공하는 시스템 allocator를 사용한다.
하지만 jemalloc 같은 좀더 발전된 형태의 커스텀 allocator를 사용해서 usecase에 맞게 할당 효율성을 올릴 수 있다. 이건 추후 별도 포스트로 정리해보겠다.



https://stackoverflow.com/questions/76866904/if-memory-fragmentation-is-no-longer-an-issue-with-64-bit-virtual-address-space
https://stackoverflow.com/questions/64349058/memory-defragmentation-heap-compaction-commonplace-in-managed-languages-but-n
https://stackoverflow.com/questions/44351461/difference-between-paging-and-segmentation
https://stackoverflow.com/questions/23584055/what-is-segmentation-and-paging-in-computer-science
https://en.wikipedia.org/wiki/Memory_segmentation
https://stackoverflow.com/questions/57222727/is-segmentation-completely-not-used-in-x64
https://stackoverflow.com/questions/40658045/does-rusts-memory-management-result-in-fragmented-memory