어제 면접을 보면서 제일 제일 기억에 남는 질문 하나가 있습니다.
자바에서는 잘 발생하지 않겠지만... 메모리 파편화가 발생하면 어떻게 해야할까요?
이 질문을 받고 저는 GC에서의 압착에 대해서 설명했습니다... 하지만 이 방법은 결국 모든 상황에서 적합하지 않고, 그저 대표적으로 JVM 환경에서 제공해주는 편한 방법에 불과하다는 답변을 받았습니다. 그러면 메모리 파편화가 발생하면 어떻게 해결하면 좋을까요? 우선 메모리 파편화가 무엇인지 먼저 살펴봐야 합니다.
메모리를 할당하고 해제하는 과정이 반복될 때, 메모리의 여유공간이 있음에도 새로 할당해주지 못할 때 메모리 파편화가 발생했다고 합니다.
예를 들면 객체를 메모리에 할당하고 해제하는 과정에서 메모리가 다음과 같은 상황이 되었다고 생각해봅시다.

위 그림에서 볼 수 있듯이 230MB의 여유 공간을 가지고 있지만, 메모리에 객체를 할당해줄 때 연속된 공간에 대해서 할당하기 때문에 100MB 이상의 객체를 할당해줄 수 없습니다(외부 파편화). 또한 100MB 보다 작은 크기의 객체를 할당할 수 있지만, 할당하고 남은 공간 또한 파편화를 유발할 수 있습니다(내부 파편화).
이러한 문제를 해결하기 위한 방법은로는 페이징 기법, 새그멘테이션 기법, 메모리 풀을 통해 해결할 수 있습니다. 이 중 애플리케이션에서도 사용할 수 있는 메모리 풀에 대해서 설명해드리겠습니다.
메모리 풀은 메모리 관리 방법 중 하나로, 메모리 공간에 애플리케이션이 사용할 메모리 공간을 미리 할당받아두고 필요할 때마다 사용하고 반납하는 방법입니다. 이를 통해 좀 더 효율적으로 메모리 공간을 관리할 수 있습니다. 메모리 풀에 대해서 자세하게 살펴보겠습니다.
메모리 풀을 사용할 때는 일반적으로 비즈니스 코드와 시스템 커널 사이에 2개의 계층으로 구성되어 있습니다. 그 중 시스템 커널과 가까운 메모리 풀은 C 라이브러리를 사용합니다.

비즈니스 코드에서 메모리로 할당 요청을 보내면 애플리케이션 계층의 메모리 풀에 여유 공간이 있다면 해당 계층에 바로 할당해줍니다. 만약 여유 공간이 없다면, 그 아래의 C 라이브러리 메모리 풀에 할당 요청을 보내게 됩니다. 자바를 예를 들면, JVM 설정 중 -Xmx, -Xms 플래그를 통해 설정하는 힙 메모리 영역이 애플리케이션 계층 메모리 풀입니다. 또한 GC 메커니즘을 사용하지 않는 off-heap 메모리 영역(Metaspace 등등)은 C 라이브러리 메모리 풀에 의해 관리되는 영역입니다.
자바에서의 예를 통해 애플리케이션 계층의 메모리 풀은 GC를 통해 효율적으로 관리할 수 있음을 쉽게 유추할 수 있습니다. 그러면 C 라이브러리로 구현된 메모리 풀은 어떻게 메모리 공간을 관리할까요? 리눅스에서 대표적으로 사용하는 Ptmalloc2를 예로 살펴보겠습니다.
C 라이브러리 메모리 풀은 요청한 객체의 크기보다 큰 크기의 공간을 메모리 풀에 할당합니다. 예를들면 프로세스가 1 byte 객체를 할당할 때, Ptmalloc2는 132KB의 공간(Main Arena라고 부름)을 미리 할당해둡니다. 이 후 프로세스에서 메모리 할당 요청을 보낼 때, 미리 할당한 132KB 공간에 할당합니다.
1 byte 크기의 객체를 해제할 때, Ptmalloc2는 OS에 해당 영역을 반납하는 대신 해당 공간을 계속 유지합니다. 그리고 다시 객체를 할당할 때, 빈 공간들을 찾아 병합하거나 나눠쓰는 방식으로 메모리를 관리합니다. 이 방법을 통해 다른 객체를 할당할 때 빠르게 할당해주며, 효율적으로 메모리를 관리할 수 있게 됩니다.(OS로부터 새 메모리 공간을 할당 받지 않기 때문입니다.) 앞선 예시는 단일 스레드 환경에서 사용되는 방식입니다. 그러면 멀티 스레드 환경에서는 어떻게 메모리 풀을 관리할까요?
멀티 스레드 환경에서는 스레드가 생성될 때마다 128MB의 공간을 할당하며 해당 공간(Sub Arena를 생성함)을 통해 메모리를 관리합니다. 즉, 스레드를 생성할 때마다 선형적으로 메모리 사용량이 증가하게 됩니다. 하지만 이는 메모리 사용량이 매우 커질 수 있기 때문에, 적은 스레드를 사용하는 환경에서 효율적입니다. 또한 자체적으로 메모리를 관리하는 JVM과 같은 환경에는 적합하지 않습니다.
Ptmalloc2에서는 MALLOC_ARENA_MAX 변수를 통해 최대 생성하는 스레드 메모리 풀을 조절할 수 있습니다. 이를 조절하게 되면 스레드를 생성할 때, 이미 생성된 공간을 공유하는 방식을 사용합니다.
그러면 질문으로 돌아와 자바에서 메모리 파편화가 발생하면 어떻게 해야할까요? 가용 메모리는 많은데, 객체를 생성하려하니 할당이 안되는 에러가 발생하는 경우 어떻게 해야할까요? GC가 발생하기까지 기다려야 할까요?
힙 사이즈를 늘리는 등 여려가지 방법이 있지만 그 중 미리 크기를 지정하여 자체 메모리 풀을 만드는 방법을 소개해드리겠습니다.
할당하지 못하는 상황을 방지하기 위해 Collection 타입의 인스턴스를 크기를 지정해서 생성한 다음, 해당 Collection을 이용하여 객체를 관리하는 방법이 있습니다. 예를 들면 Map 인스턴스를 생성할 때 예를 들어보겠습니다.
Map<String, HugeSizeObject> mapWithInitialCapacity = new HashMap<>(500);
500개의 객체를 저장할 수 있는 맵을 초기화해서 사용한다면, 객체를 할당, 해제하는 과정이 빈번하게 발생하더라도 미리 점유한 메모리 공간만하기 때문에 객체 할당이 불가능해지는 경우가 없습니다. 또한 미리 할당된 공간을 사용하기 때문에 접근 시간에도 이점이 있습니다.
하지만 메모리 풀을 사용할 때 장점만 있을까요?
사실 메모리를 점유한다는 뜻은 해당 공간이 낭비가 될 수 있습니다. 즉, 공간을 점유하고 사용하지 않는다면 공간 낭비가 되는 것입니다. 반대로 객체를 할당하고 계속 방치한다면 메모리 누수가 발생할 수 있습니다. 즉, 동시에 생성되는 객체의 수를 적절히 추적한 다음 컬랙션 크기를 설정하는 것을 권장합니다.
참고
https://medium.com/better-programming/c-memory-pool-and-small-object-allocator-8f27671bd9ee
https://blog.devgenius.io/memory-pool-how-to-improve-memory-allocation-efficiency-57723c8d8630
https://bugs.openjdk.org/browse/JDK-8193521
https://github.com/cloudfoundry/java-buildpack/issues/320
윈도우에서 조각모음 하면 안되나요? 신입 개발자가 도대체 어디까지 알아야하는건가요? 취업을 시켜주겠다는건가요 말겠다는건가요?