
JEP 546은 Z Garbage Collector(ZGC)가 시스템 자원과 애플리케이션의 요구에 맞춰 힙 크기를 자동으로 조절하는 정책을 제안한다. 이 정책을 켜면 메모리 사용량과 가비지 컬렉션(GC) CPU 사용량의 균형을 맞추고, 같은 시스템의 다른 애플리케이션과 자원을 나눠 쓰도록 힙을 확장하거나 축소한다.
제안 배경과 설정 방식
자바 가상 머신(JVM)은 무한한 힙이 있는 것처럼 보이게 하지만, 실제로는 컴퓨터의 한정된 물리 메모리와 하드웨어·운영체제의 기능을 이용해 이를 구현한다. ZGC는 추적형 가비지 컬렉터다. 정적 필드와 스택 프레임의 지역 변수 같은 루트에서 시작해 참조를 따라가며 살아 있는 객체를 찾는다. 살아 있는 객체가 적은 기존 힙 영역에서 객체를 새 영역으로 복사해 힙을 압축하고, 기존 영역을 해제한다.
따라서 GC 한 주기의 CPU 비용은 살아 있는 객체의 수와 크기에 따라 달라지며, 현재 자바 객체를 저장할 수 있는 힙 크기와는 무관하다. ZGC는 힙이 거의 찼다고 판단할 때 GC를 시작한다. 다른 조건이 같다면 힙이 클수록 GC 주기가 덜 자주 발생해 장기적인 GC CPU 사용량은 줄고, 힙이 작을수록 주기가 잦아져 CPU 사용량은 늘어난다.
ZGC는 CPU 사용량을 줄이기 위해 가능한 한 많은 메모리를 사용하며, 설정된 최대 힙 크기까지 확장한다. 기본적으로 HotSpot JVM은 호스트 물리 메모리의 25%를 최대 힙 크기로 사용한다. 예를 들어 물리 메모리가 128GB인 시스템에서는 힙에 32GB를 사용한다. 이 기본값이 합리적일 수는 있지만 모든 애플리케이션에 최적인 최대 힙 크기는 없다.
최대 힙 크기는 -Xmx로 지정할 수 있다. 적절한 값을 고르려면 대표적인 작업 부하를 재현하는 실험 환경에서 여러 크기를 적용하고 메모리 사용량, 처리량, 지연 시간을 측정해야 한다. 이런 실험은 제대로 수행하기 어렵고 실제로는 드물다. 실험하더라도 애플리케이션이나 JVM을 업데이트할 때마다 다시 해야 하며, 결과를 다른 하드웨어에 그대로 적용할 수도 없다.
최대 힙이 너무 작으면 ZGC가 더 자주 GC를 수행해 CPU 시간을 더 쓰고 성능이 떨어지며, 애플리케이션의 메모리가 부족해질 수도 있다. 반대로 최대 힙이 너무 크면 운영체제의 물리 메모리가 부족해질 수 있다. 운영체제가 가상 메모리 페이지를 디스크로 내보내면 시스템 전체 성능이 떨어지고, 최악의 경우 물리 메모리를 확보하려고 프로세스를 종료할 수 있다.
최대 힙 크기는 넘을 수 없는 상한이다. 다른 프로세스를 배려하면서 여유 메모리도 확보하도록 ZGC에 소프트 최대 힙 크기를 설정할 수 있다. ZGC는 GC를 더 자주 수행해 힙을 소프트 최대값 아래에 두려 하지만, 할당을 멈추게 하거나 애플리케이션의 메모리 부족을 일으킬 상황이라면 하드 최대값까지 힙을 확장할 수 있다. 할당 정지는 ZGC가 지원하려는 지연 시간 민감형 애플리케이션에 치명적이다. 소프트 최대값은 다른 프로세스에 메모리를 남기면서 예측하지 못한 할당 급증에 대비할 메모리도 확보하게 한다.
예를 들어 애플리케이션이 보통 2GB 힙으로 잘 동작하지만, 때때로 5GB가 필요한 수요 급증을 겪는다고 하자. 최대 힙을 5GB로 설정하면 다른 프로세스에 영향을 줄 수 있다. 대신 최대 힙을 5GB, 소프트 최대 힙을 2GB로 설정하면 평소에는 2GB 아래를 유지하고, 작업 부하가 급증할 때 최대 5GB까지 확장한 뒤 급증이 끝나면 2GB로 줄어든다.
-Xmx5G -XX:SoftMaxHeapSize=2G
하지만 소프트 최대 힙 크기를 적절히 설정하는 일도 하드 최대 힙 크기 설정만큼 어렵고, 잘못 고르면 비슷한 부작용이 생길 수 있다. 제안된 적응형 정책은 측정한 자원 수요와 가용량을 바탕으로 힙 크기를 자동 조정한다. 가능할 때는 메모리를 더 써서 GC CPU 사용량을 낮추고, 필요할 때는 메모리를 덜 쓰는 대신 GC CPU 사용량을 높인다. 이를 통해 수동 GC 튜닝의 필요성을 줄이고, 예상치 못한 작업 부하 변화에 대응하며, 애플리케이션·JVM 업데이트와 하드웨어 구성 변화에도 더 잘 적응하는 것을 목표로 한다.
적응형 힙 크기 조절과 사용법
제안은 선택적으로 켤 수 있는 정책이다. 활성화하면 ZGC는 물리 메모리의 100%에서 작은 예비분을 뺀 크기를 기본 최대 힙 크기로 사용하고, 메모리 사용량과 GC CPU 사용량의 균형을 맞추도록 힙을 조절한다. 또한 호스트 시스템 전체의 메모리와 CPU 사용량 변화에 대응해 힙을 확장하거나 축소한다. 이는 소프트 최대 힙 크기를 설정한 것처럼 동작한다.
시작과 워밍업에 관한 관련 개선과 함께 적용하면 -Xmx나 -XX:SoftMaxHeapSize를 설정할 필요도, 설정하는 것이 바람직하지도 않게 된다는 제안이다. 애플리케이션을 실행할 때 ZGC를 선택하고 적응형 힙 크기 조절을 켜면 된다.
$ java -XX:+UseZGC -XX:+ZAdaptiveHeapSizing ...
대부분의 애플리케이션은 ZGC 자원 사용량을 따로 조정할 필요가 없으며 기본값으로 잘 동작할 것이라고 제안은 설명한다. 필요하다면 명령행 옵션으로 GC 목표 강도를 지정해 메모리 사용량과 GC CPU 사용량의 균형을 수동으로 조절할 수 있다. 강도는 양의 정수이며 기본값은 5다. 값을 낮추면 ZGC가 GC 주기를 덜 자주 시작해 CPU 시간을 덜 쓰는 대신 힙 크기와 물리 메모리 사용량이 늘어난다. 값을 높이면 GC 주기가 잦아져 CPU 시간을 더 쓰는 대신 힙 크기와 물리 메모리 사용량이 줄어든다.
기본 설정보다 CPU 시간을 조금 덜 쓰고 메모리를 조금 더 쓰려면 다음과 같이 설정한다.
$ java -XX:+UseZGC -XX:+ZAdaptiveHeapSizing -XX:ZGCIntensity=4 ...
강도는 사용자의 의도를 나타내기 위한 추상적인 GC CPU 사용량 지표다. 강도와 실제 GC CPU 사용량 사이의 관계는 의도적으로 명시하지 않았다. 이를 통해 향후 ZGC의 자동 튜닝 정책을 개선하면서 관계를 조정할 수 있고, 추상적인 강도 지표는 하드웨어 구성 간 이식성도 높인다.
애플리케이션이 객체를 만들고 ZGC가 이를 수집하면서 살아 있는 객체가 차지하는 힙 메모리는 계속 변한다. 애플리케이션이 워밍업하는 동안 ZGC는 수요에 앞서기 위해 힙을 빠르게 확장한다. 이후에는 목표 강도에 맞춰 힙 크기를 반복해서 조절한다. GC CPU 사용량이 목표 강도가 나타내는 수준보다 높으면 다음 GC 주기가 끝날 때 힙을 확장한다. 그러면 CPU 사용량은 목표 수준을 향해 낮아지고 처리량은 개선된다. 반대로 CPU 사용량이 목표 수준보다 낮으면 다음 주기 종료 시 힙을 축소한다. 이때 CPU 사용량은 목표 수준을 향해 높아지고 처리량은 낮아진다.
급격한 변동을 피하기 위해 힙 크기 조정은 시간에 걸쳐 완만하게 이뤄진다. 이 방식은 타바콜리소메(Tavakolisomeh) 등이 발표한 Heap Size Adjustment with CPU Control 논문에 설명돼 있다.
ZGC는 세대별 가비지 컬렉터다. 새 객체는 힙의 젊은 세대에 놓이고, 오래 살아남은 객체는 구세대로 승격된다. 젊은 세대에서는 잦은 마이너 GC를, 전체 힙에서는 덜 잦은 메이저 GC를 수행한다. ZGC는 힙 전체 크기뿐 아니라 젊은 세대와 구세대의 크기도 재조정해 목표 강도를 맞춘다. 일련의 마이너 GC로 GC CPU 사용량이 목표 수준을 넘으면 힙을 즉시 확장한다.
다른 애플리케이션과 자원 나누기
ZGC가 메모리나 CPU 시간을 제한 없이 사용하면 호스트 시스템의 다른 프로세스가 쓸 자원이 부족해질 수 있다. 그래서 ZGC는 애플리케이션 동작뿐 아니라 JVM 프로세스가 호스트 시스템의 메모리와 CPU 시간에서 차지하는 비율도 살핀다. JVM이 메모리의 상당 부분을 쓰면서 CPU 시간은 적게 쓰면 힙을 줄여 CPU 사용량을 높인다. 반대로 메모리는 적게 쓰고 CPU 시간은 많이 쓰면 힙을 늘려 CPU 사용량을 낮춘다.
ZGC는 목표 강도 달성보다 다른 프로세스를 배려하는 일을 우선한다. 이를 위해 상황에 따라 기본 또는 지정된 목표 강도를 높이거나 낮춘다. 같은 호스트 시스템에서 ZGC를 사용하는 여러 JVM은 각자의 목표 강도에 부합하는 방식으로 메모리와 CPU 시간을 사용하게 된다.
제안에 포함된 사례에서는 메모리 16GB 컨테이너를 만들고 SPECjbb2015 벤치마크 애플리케이션 한 인스턴스를 실행한 뒤 H2 인메모리 데이터베이스 벤치마크를 실행했다. 데이터베이스 벤치마크가 끝난 후에는 SPECjbb2015 인스턴스 두 개를 더 실행했다. 42분 동안 컨테이너와 JVM 네 개의 물리 메모리 사용량을 관찰했다.
처음 12분 동안 첫 SPECjbb2015 인스턴스는 초당 5,000건의 요청을 처리했다. ZGC는 컨테이너의 메모리 제한 안에서 성능을 낼 수 있는 적절한 힙 크기를 찾았다. ZGC의 CPU 사용량이 과도하지 않아 힙은 천천히 확장됐다.
다음 12분 동안 H2 데이터베이스 애플리케이션이 첫 SPECjbb2015 인스턴스와 함께 실행됐다. 데이터베이스 JVM은 초기에 힙을 빠르게 확장했고, 이에 대응해 SPECjbb2015 JVM은 즉시 힙을 줄였다. 그 결과 메모리를 많이 쓰는 데이터베이스 JVM이 가용 메모리에서 더 큰 몫을 확보했다. 이 기간에도 SPECjbb2015 인스턴스는 CPU를 포화시키지 않으면서 초당 5,000건의 요청을 처리했다. 다만 GC CPU 사용량은 높아졌고, 지연 시간과 처리량은 모두 저하됐다.
24분 시점에 데이터베이스 애플리케이션이 종료되자 첫 SPECjbb2015 JVM은 데이터베이스가 사용하던 메모리를 이용하도록 힙을 확장했다. ZGC는 CPU 사용량을 줄이기 위해 힙을 빠르게 늘렸고, 이에 따라 지연 시간과 처리량이 개선됐다.
30분부터 시작된 마지막 단계에서는 SPECjbb2015 인스턴스 두 개가 기존 인스턴스와 함께 실행됐으며, 각 인스턴스는 초당 5,000건의 요청을 처리했다. 2분 이내에 JVM 세 개가 가용 메모리를 고르게 나눠 각 인스턴스가 좋은 성능을 내도록 했다.
다른 언어 구현체의 일부 가비지 컬렉터는 사용 중인 데이터 크기와 전체 힙 크기의 비율인 목표 메모리 상주율을 설정해 메모리와 GC CPU 사용량의 균형을 조절한다. 그러나 같은 상주율에서도 GC CPU 사용량은 크게 달라질 수 있다. 세대별 GC에서는 사용 중인 데이터의 양을 측정하는 일도 어렵다. GC CPU 사용량은 더 직접적으로 측정할 수 있을 뿐 아니라 사용자가 관찰하고 중요하게 여기는 지표에 더 가까운 대안이라고 제안은 설명한다.
위험과 전제
적응형 힙 크기 조절을 켜면 ZGC의 기본 최대 힙 크기는 기존 물리 메모리의 25%에서, 물리 메모리의 100%에서 작은 예비분을 뺀 값으로 바뀐다. 따라서 특정 애플리케이션에서 과거 릴리스보다 더 많은 메모리를 사용해 다른 프로세스에 메모리가 부족해질 위험이 있다. 다만 기존 기본값을 사용하더라도 같은 기본값을 쓰는 JVM 여러 개가 동시에 실행되면 다른 프로세스의 메모리가 부족해질 위험은 이미 있었다.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요