정지 시간을 힙 크기와 무관하게 밀리초 수준으로 유지하는 동시 수집기.
-
G1 — 기본값. 힙을 Region 으로 나누고, 쓰레기가 많은 Region 부터 고른다
- (Garbage-First) 목표 정지 시간을 주면 그에 맞춰 수집량을 조절
- 정지 시간 수십~수백 ms
-
ZGC — 이동(복사)까지 애플리케이션과 동시에 수행한다
- 정지 시간 1ms 미만, 힙이 수 TB 여도 유지된다
-
Shenandoah — 목표가 비슷하다 (Red Hat 주도)
어떻게 이동을 동시에 하나
객체를 옮기는 동안 애플리케이션이 옛 주소를 읽으면 안 된다
ZGC 는 참조 안의 사용하지 않는 비트에 상태를 새긴다 (컬러 포인터) + 읽기 배리어: 참조를 읽을 때마다 "이미 옮겨졌는가" 를 검사하고
-
옮겨졌으면 새 주소로 고쳐 준다 (self-healing)
-
STW 는 루트 스캔 같은 아주 짧은 구간만 남는다
공짜가 아니다
읽기 배리어 비용 → 처리량이 G1 대비 몇 % 떨어질 수 있다 메모리 사용량이 더 크다
즉 지연 시간을 처리량·메모리와 맞바꾸는 것이다
실무 선택 기준
- 힙 8GB 이하 · 일반 웹 서비스 G1 (기본값으로 충분)
- 지연 시간 SLA 가 엄격 (p99 중요) — ZGC
- 힙 32GB 이상 · 정지가 곧 사고 ZGC
- 처리량이 최우선 · 배치 — Parallel GC
- 짧게 살고 끝나는 프로세스 — Serial / Epsilon
"기본값으로 시작하고, 정지 시간이 실제 문제일 때 바꾼다" 가 순서다 측정 없이 GC 를 바꾸는 것은 대체로 손해다
압축 참조 경계
힙이 32GB를 넘으면 압축 참조가 꺼져 참조가 4→8바이트가 된다. 31GB와 33GB의 실효 용량이 역전될 수 있어, 힙 증설 시 이 경계를 반드시 확인한다.
배리어의 위치가 설계를 가른다
- G1 — 쓰기 배리어 중심 — 참조를 '쓸 때' 카드/큐를 갱신한다
- 읽기가 잦은 워크로드에 유리
- ZGC — 읽기 배리어 중심 — 참조를 '읽을 때' 색을 검사하고 고친다
- 이동을 동시에 하려면 읽는 쪽을 잡아야 하기 때문이다
- Shenandoah — 브룩스 포인터(간접 참조) → 이후 로드 참조 배리어로 전환
"어느 시점에 비용을 낼 것인가" 의 선택이고, 그래서 워크로드에 따라 승패가 갈린다
컨테이너에서 반드시 확인할 것
JVM 은 컨테이너 메모리 limit 을 인식한다 (Java 10+, UseContainerSupport) -XX:MaxRAMPercentage=75 물리 대신 limit 기준 비율로 힙 결정 -Xmx 를 직접 주면 limit 변경 시 따라가지 않는다
주의: 힙 밖에도 메모리가 든다
- 메타스페이스 · 코드 캐시 · 스레드 스택 · DirectByteBuffer · GC 자체 구조
- 힙을 limit 의 100% 로 잡으면 컨테이너가 OOMKill 된다
- (JVM 의 OutOfMemoryError 가 아니라 커널이 죽인다 — 로그도 안 남는다)
면접 함정
- ❌ "ZGC는 STW가 아예 없다" → 매우 짧은 구간이 남는다.
- ❌ "최신 GC가 항상 낫다" → 처리량은 오히려 떨어질 수 있다.