이번 포스팅에서는 적절한 Heap Size에 대해서 고민해 보려고 한다.
이전 포스팅들에서 살펴본 내용의 주제는 다음과 같다.
1. JVM 구성요소 3가지(Class loader subsystem, Runtime data area, excution engine)
2. java memory model(heap, non-heap:[stack, code cache, metaspace, ...])
3. Garbage Collection과 종류별 동작과정
오늘은 적절한 Heap Size를 판단하기 위해 어떤것을 고려해야 할지에 대해 정리하고자 한다.
일반적으로 Heap Size가 작다면 큰 Heap Size와 비교해서 GC가 빈번하게 발생할 것이다. 반면 Heap Size가 크다면 한번 GC가 수행될 때 수거해야할 garbage가 상대적으로 많기 때문에 작은 Heap Size에 비해 STW 시간이 클 것이다.
예를 들어 compact를 수행하지 않는 CMS GC의 경우 큰 Heap Size에서는 CMF가 발생했을 때 STW가 굉장히 길어질 수 있다. 반면 작은 Heap Size에서는 큰 Heap Size에 비해 CMF가 발생할 확률이 높을 것이다.
Heap Size를 수백 테라바이트로 할당할 수 있다고 가정하고 CMS GC를 사용한다고 가정해보자 이 경우 CMF가 발생할 확률이 아주 낮아지게 되므로 문제가 없다고 생각할 수 있지만 그렇지 않다. Java기반의 application은 Heap Size에 비례하여 성능이 좋아지지 않는다.
JVM은 Object의 reference를 관리하기 위해 OOP(Ordinary Object pointer)라는 자료구조를 가진다. JVM은 32bit와 64bit가 존재한다. 주소값을 표기하기 위해 32bit를 사용하면 oop는 최대힙을 4GB까지 사용이 가능하고 64bit를 사용하면 16EB까지 사용이 가능하다.
현실적으로 16EB까지 메모리를 사용할 수 없을 뿐만 아니라, 64bit 포인터로 공간을 관리할 경우 문제가 발생한다.
그렇다고 32bit 포인터를 사용하자니 4GB는 너무 작다. HotSpot JVM은 이러한 문제를 해결하기 위해 64bit JVM에서 32bit 포인터를 이용하는 것과 유사하면서도 최대 힙 크기를 32GB까지 사용할 수 있도록 Compressed Ordinary Object Pointer를 지원한다.
Compressed Ordinary Object Pointer는 64bit 포인터를 32bit object offset으로 인코딩 및 디코딩하여 32bit 포인터를 사용하는 것처럼 지원한다. 이를 통해 64bit JVM에서도 32GB까지의 Heap Size에서는 32bit 포인터의 이점을 살릴 수 있게 지원한다. 64bit JVM을 사용할 때 Heap Size가 32GB보다 작으면 자동으로 이 Compressed OOP를 이용하고 32GB보다 Heap Size가 커지게 되면 자동으로 64bit의 OOP를 사용한다.
JVM이 OS에 virtual adress가 0부터 시작하는 java heap용 메모리를 요청하여 Compressed OOP를 사용하는 경우이다. java heap의 base address를 더하는 연산을 할 필요가 없기 때문에 성능에 좀 더 이점이 있다.
결론 까지 왔지만 아직까지 아리까리 하다. 이 포스팅의 결론은 "32GB 이하의 Heap Size를 유지해야 한다"가 아니다. ZGC의 경우 Compressed OOP를 지원하지 않는다. 서버에서 동작할 application의 특성을 파악하여 큰 Heap Size가 필요하고 Compressed OOP를 포기할만 하다면 ZGC와 32GB를 초과하는 Heap Size를 설정할 수도 있을 것이다. 그러나 그렇지 않다면 32GB 이하의 Heap Size안에서 여러 GC를 적용해보거나 Heap Size를 조금씩 변경하며 Log를 확인해 최적의 Heap Size를 찾아야 할 것이다.