같은 문제, 다른 목표
두 수집기 모두 큰 힙을 다루지만 겨냥하는 지점이 다르다.
- G1 — 정지 시간을 예측 가능한 범위로 관리하면서 처리량도 지킨다
- ZGC — 정지 시간을 힙 크기와 무관하게 수 밀리초 이하로 누른다
G1 — 쓰레기가 많은 구역부터
G1은 힙을 크기가 같은 region 수천 개로 나눈다. Eden, Survivor, Old를 연속된 큰 덩어리가 아니라 region 집합으로 관리한다.
수집할 때는 전부 훑지 않는다. 각 region의 쓰레기 비율을 추적해두고 회수량 대비 비용이 좋은 region부터 고른다. 이름 그대로 garbage first다.
-XX:MaxGCPauseMillis로 목표 정지 시간을 주면, G1은 그 예산 안에서 처리할 수 있는 만큼만 region을 골라 수집한다. 목표를 너무 낮게 잡으면 한 번에 조금씩만 회수하므로 GC가 잦아지고 처리량이 떨어진다.
ZGC — 애플리케이션을 멈추지 않고 옮긴다
객체를 이동시키는 작업이 정지의 주범이다. 참조를 전부 새 주소로 고쳐야 하기 때문이다.
ZGC는 포인터 자체에 표시(colored pointer)를 심고, 참조를 읽는 순간 배리어가 개입해 필요하면 그 자리에서 새 주소로 고친다. 그래서 이동과 참조 갱신을 애플리케이션과 동시에 진행할 수 있다.
정지가 필요한 구간은 루트 스캔 정도로 좁혀지고, 이 비용은 힙 크기에 비례하지 않는다. 힙이 수십 GB든 수 TB든 정지 시간이 비슷한 이유다.
무엇을 내주었나
공짜는 아니다. 참조를 읽을 때마다 배리어가 끼어들므로 처리량이 다소 손해를 본다. 메모리도 더 쓴다.
어떻게 고를까
- 응답 지연이 사용자 경험에 직결되고 힙이 크다 → ZGC
- 배치성 작업이라 총 처리량이 중요하다 → G1
- 판단이 어렵다 → G1으로 시작해 정지 시간을 측정하고, 목표를 못 맞출 때 옮긴다
정리
수집기 선택은 “무엇이 더 좋은가”가 아니라 정지 시간과 처리량 중 무엇을 먼저 지킬 것인가의 문제다.