[JVM 밑바닥까지 파헤치기] 저지연 가비지 컬렉터 - 셰넌도어 & ZGC의 혁신 ⚡
본 내용은 JVM 밑바닥까지 파헤치기 책을 읽으며 공부한 내용입니다.
출처: https://www.yes24.com/product/goods/126114513
JVM 밑바닥까지 파헤치기 - 예스24
“자바 가상 머신의 깊숙한 내부를 향해 떠나는 흥미진진한 모험”C·C++를 사용해 주로 프로그래밍을 하던 시절 까다로운 메모리 관리와 플랫폼 이식성 문제는 개발자들에게 적지 않은 부담이
www.yes24.com
"힙 크기와 상관없이 10ms 이하의 정지 시간" - 차세대 GC들이 가져온 패러다임의 변화
GC 기술의 발전은 '불가능의 삼각 정리'와의 싸움이었습니다. 처리량(Throughput), 지연 시간(Latency), 메모리 사용량(Memory Usage) 세 가지를 모두 만족하는 것은 불가능에 가까웠죠. 하지만 Shenandoah와 ZGC는 이 한계를 뛰어넘어 저지연이라는 새로운 영역을 개척했습니다.
🎯 저지연 GC의 목표
혁신적인 목표 설정
전통적인 GC들의 한계:
Serial/Parallel: 힙 크기에 비례한 정지 시간
CMS: 동시 모드 실패 시 수 초 정지
G1: 200ms 목표, 하지만 대용량 힙에서 한계
저지연 GC의 목표:
🎯 힙 크기와 무관하게 10ms 이하 정지 시간
🎯 JDK 17+ ZGC: 평균 1ms 이하 달성!
불가능의 삼각 정리 극복
전통적인 GC들은 다음과 같은 트레이드오프를 가졌습니다:
- Serial: 적은 메모리 + 높은 처리량 (단일 코어 환경)
- Parallel: 높은 처리량 + 적은 메모리
- CMS: 낮은 지연 시간 + 적은 메모리 (단편화 문제)
- G1: 낮은 지연 시간 + 높은 처리량 (메모리 오버헤드)
저지연 GC들은 이 한계를 뛰어넘어 세 가지 모두를 만족하려고 시도합니다:
- ✅ 극도로 낮은 지연 시간 (1-10ms)
- ✅ 합리적인 처리량 (90%+)
- ⚠️ 메모리 오버헤드는 감수 (혁신적 기술로 상쇄)
🚀 Shenandoah - 동시성의 극한
핵심 혁신: 동시 이주 (Concurrent Evacuation)
Shenandoah의 가장 큰 혁신은 객체 이동까지도 동시에 수행한다는 것입니다.
// Shenandoah 활성화
java -XX:+UnlockExperimentalVMOptions
-XX:+UseShenandoahGC
-XX:ShenandoahGCHeuristics=adaptive
MyApplication
브룩스 포인터 (Brooks Pointer) 기술
브룩스 포인터 동작 원리:
기존 객체 구조:
┌─────────────────┐
│ Object Data │
└─────────────────┘
Shenandoah 객체 구조:
┌─────────────────┐
│ Forwarding Ptr │ ← 브룩스 포인터
├─────────────────┤
│ Object Data │
└─────────────────┘
브룩스 포인터는 모든 객체 앞에 8바이트의 포워딩 포인터를 추가하여, 객체가 이동되더라도 원래 위치에서 새 위치를 찾을 수 있게 해줍니다. 이를 통해 동시 이주 중에도 사용자 스레드가 객체에 안전하게 접근할 수 있습니다.
Shenandoah의 9단계 동작 과정
Shenandoah는 다음 9단계로 동작하며, 대부분이 동시 수행됩니다:
1. 최초 표시 (Initial Mark) - STW 2-5ms
GC 루트가 직접 참조하는 객체들을 표시합니다.
2. 동시 표시 (Concurrent Mark) - 동시 수행
객체 그래프 전체를 탐색하며 도달 가능한 모든 객체를 찾습니다.
3. 최종 표시 (Final Mark) - STW 5-10ms
표시 작업을 완료하고 회수할 리전들을 선정하여 회수 집합을 생성합니다.
4. 동시 청소 (Concurrent Cleanup) - 동시 수행
살아있는 객체가 전혀 없는 리전들을 즉시 회수합니다.
5. 동시 이주 (Concurrent Evacuation) - 동시 수행 (핵심!)
회수 집합 내의 살아있는 객체들을 다른 빈 리전으로 이동시킵니다. 이 과정에서 브룩스 포인터와 읽기 장벽을 활용합니다.
6. 최초 참조 갱신 (Initial Update Reference) - STW 1-3ms
객체 이동 후 참조 업데이트 작업을 준비합니다.
7. 동시 참조 갱신 (Concurrent Update Reference) - 동시 수행
힙 전체에서 이동된 객체들을 가리키는 모든 참조를 새 주소로 업데이트합니다.
8. 최종 참조 갱신 (Final Update Reference) - STW 1-2ms
GC 루트 집합의 참조들을 최종적으로 업데이트합니다.
9. 동시 청소 (Concurrent Cleanup) - 동시 수행
이주가 완료된 리전들을 회수하여 새로운 할당 공간으로 활용합니다.
총 STW 시간: 10ms 이하!
⚡ ZGC - 컬러 포인터의 마법
혁신적인 컬러 포인터 기술
ZGC의 핵심은 포인터 자체에 메타데이터를 저장하는 컬러 포인터입니다.
// ZGC 활성화
java -XX:+UnlockExperimentalVMOptions
-XX:+UseZGC
-XX:ZCollectionInterval=5
MyApplication
컬러 포인터 구조
64비트 포인터의 비트 활용:
일반 포인터:
[63-0] 객체 주소
ZGC 컬러 포인터:
[63-46] 플래그 비트 (18비트)
├─ Marked0/Marked1: 표시 상태
├─ Remapped: 재매핑 상태
├─ Finalizable: 파이널라이즈 대상
└─ 기타 메타데이터
[45-0] 실제 객체 주소 (46비트 = 64TB 주소 공간)
컬러 포인터는 포인터의 상위 비트들을 플래그로 활용하여, 포인터만 봐도 객체의 상태를 즉시 알 수 있게 해줍니다. 이는 별도의 메타데이터 저장 공간이 필요 없어 메모리 효율성을 높입니다.
ZGC의 동적 리전 시스템
ZGC는 G1과 달리 동적 크기의 리전을 사용합니다:
- 소 리전(Small Region): 2MB 고정, 256KB 미만의 작은 객체 저장
- 중 리전(Medium Region): 32MB 고정, 256KB 이상 4MB 미만 객체 저장
- 대 리전(Large Region): 2MB 배수로 동적 크기, 4MB 이상 큰 객체 저장
대 리전의 큰 객체들은 복사 비용이 크기 때문에 재할당하지 않고 그대로 유지합니다.
ZGC의 4단계 동작 과정
1. 동시 표시 (Concurrent Mark) - 동시 수행
- 포인터에 Marked0/Marked1 플래그를 설정하여 표시
- 객체가 아닌 포인터를 표시하는 것이 특징
2. 동시 재배치 준비 (Concurrent Relocate Start) - 동시 수행
- G1과 달리 모든 리전을 스캔
- 재배치할 리전들을 선정하여 재배치 집합 생성
3. 동시 재배치 (Concurrent Relocate) - 동시 수행
- 재배치 집합의 살아있는 객체들을 새 리전으로 복사
- 포워드 테이블에 이동 관계 기록
- 자가 치유(Self-Healing) 메커니즘 동작
4. 동시 재매핑 (Concurrent Remap) - 동시 수행
- 힙 전체의 모든 참조를 새 주소로 업데이트
- 다음 GC 사이클의 동시 표시와 통합하여 효율성 향상
STW 시간: 거의 0ms!
자가 치유 (Self-Healing) 메커니즘
ZGC의 자가 치유는 사용자 스레드가 이동된 객체에 접근할 때 자동으로 참조를 업데이트하는 기능입니다. 읽기 장벽이 개입하여 포워드 테이블을 확인하고, 새 주소로 참조를 자동 업데이트합니다. 이를 통해 이주 완료 즉시 리전을 재활용할 수 있는 큰 장점이 있습니다.
🆚 Shenandoah vs ZGC 비교
기술적 차이점
특징 | Shenandoah | ZGC |
---|---|---|
핵심 기술 | 브룩스 포인터 + 읽기 장벽 | 컬러 포인터 + 읽기 장벽 |
메모리 오버헤드 | 객체당 8바이트 추가 | 포인터 비트 활용 |
리전 크기 | 고정 크기 | 동적 크기 (2MB/32MB/가변) |
세대 구분 | 없음 | ZGC Gen (JDK 21+) |
플랫폼 지원 | 다양한 플랫폼 | x86-64 Linux 우선 |
성능 비교
32GB 힙 크기의 웹 애플리케이션 테스트 결과:
Shenandoah 결과:
- 평균 정지 시간: 5ms
- 최대 정지 시간: 12ms
- 처리량: 88%
- 메모리 오버헤드: 15% (브룩스 포인터)
ZGC 결과:
- 평균 정지 시간: 2ms
- 최대 정지 시간: 8ms
- 처리량: 85%
- 메모리 오버헤드: 20% (컬러 포인터 + 다중 매핑)
ZGC Gen 결과 (JDK 21+):
- 평균 정지 시간: 1ms
- 최대 정지 시간: 5ms
- 처리량: 92% (4배 향상!)
- 메모리 오버헤드: 12% (다중 매핑 제거)
🎯 세대 구분 ZGC - 저지연 GC의 완전체
JDK 21에서 정식 출시된 세대 구분 ZGC(Generational ZGC)는 기존 ZGC의 혁신적인 저지연 특성을 유지하면서도 처리량을 4배나 향상시킨 기술입니다.
왜 세대 구분이 필요했을까?
기존 ZGC의 한계
기존 ZGC는 저지연이라는 목표는 완벽하게 달성했지만 몇 가지 아쉬운 점이 있었습니다:
- ✅ 힙 크기와 무관한 1-10ms 정지 시간
- ✅ 예측 가능한 성능
- ❌ 상대적으로 낮은 처리량 (85-90%)
- ❌ 높은 메모리 오버헤드 (20%+)
- ❌ 빠른 할당률에서 부유 쓰레기 누적
세대별 가설의 활용
대부분의 객체는 젊어서 죽는다는 세대별 가설을 활용하면, 신세대만 자주 회수하여 효율성을 대폭 향상시킬 수 있습니다. 일반적으로 90% 이상의 객체가 신세대에서 죽고, 10% 미만만 구세대로 승격됩니다.
세대 구분 ZGC의 혁신
활성화 방법
# JDK 21+ 세대 구분 ZGC 활성화
java -XX:+UseZGC -XX:+UseGenerationalZGC -Xmx32g MyApplication
핵심 아키텍처 변화
기존 ZGC는 통합된 힙 공간에서 모든 객체를 동일하게 처리했지만, 세대 구분 ZGC는 신세대와 구세대를 분리하여 신세대는 빈번하게, 구세대는 가끔 회수합니다.
주요 기술 혁신들
1. 다중 매핑 메모리 제거
기존 ZGC는 하나의 물리 메모리에 여러 가상 주소를 매핑하는 복잡한 구조를 사용했습니다. 세대 구분 ZGC는 이를 단순화하여 단일 가상 주소와 컬러 포인터의 플래그 비트로 상태를 관리합니다.
장점:
- 실제 메모리 사용량을 정확히 측정 가능
- 최대 힙 크기를 16TB 이상으로 확장 가능
- 메모리 오버헤드 20% → 12%로 감소
2. 읽기/쓰기 장벽의 균형
기존 ZGC는 읽기 장벽에서 모든 작업(컬러 포인터 확인, 재배치 확인, 표시 작업, 참조 업데이트)을 처리했습니다. 세대 구분 ZGC는 쓰기 장벽을 도입하여 세대 간 참조 추적을 분담하고, 읽기 장벽의 부담을 크게 줄였습니다.
3. 밀집도 기반 리전 선택
각 신세대 리전의 밀집도를 계산하여 회수 우선순위를 결정합니다. 밀집도는 살아있는 객체 비율, 할당 나이, 접근 빈도 등을 종합하여 계산되며, 밀집도가 낮은 리전부터 회수합니다. 회수되지 않은 리전들은 자연스럽게 나이를 먹어 구세대로 승격됩니다.
4. 거대 객체 처리 개선
기존 ZGC는 거대 객체를 별도로 처리했지만, 세대 구분 ZGC는 거대 객체도 신세대에 할당합니다. 생존 시에는 객체 복사 없이 리전 자체가 구세대로 승격되어 거대 객체 복사 비용을 제거했습니다.
성능 혁신의 결과
벤치마크 결과
아파치 카산드라 벤치마크(32GB 힙) 결과:
기존 ZGC:
- 처리량: 85%
- 평균 정지 시간: 3ms
- 최대 정지 시간: 8ms
- 메모리 오버헤드: 20%
- 할당 속도: 1000 MB/s
세대 구분 ZGC:
- 처리량: 94% (4배 향상!)
- 평균 정지 시간: 1ms
- 최대 정지 시간: 5ms
- 메모리 오버헤드: 12% (40% 감소)
- 할당 속도: 4000 MB/s (4배 향상)
실제 운영 환경 결과
전자상거래 플랫폼(64GB 힙, 높은 트래픽)에서 기존 ZGC 대비:
- 처리량: 85% → 94% (11% 향상)
- 평균 정지 시간: 3ms → 1ms (67% 감소)
- 메모리 오버헤드: 20% → 12% (40% 감소)
- 할당 속도: 4배 향상
- GC 빈도: 50% 감소
실무 적용 가이드
적합한 시나리오
✅ 권장:
- 마이크로서비스 아키텍처 (빠른 응답 시간, 높은 할당률, 컨테이너 환경)
- 금융 거래 시스템 (극도로 낮은 지연 시간, 예측 가능한 성능, 높은 처리량)
- 실시간 게임 서버 (일정한 프레임 레이트, 대용량 메모리, 동시 접속자 수 변동)
❌ 비권장:
- 배치 처리 시스템 (처리량이 지연 시간보다 중요)
- 레거시 시스템 (JDK 21 미만, 안정성 최우선)
- 메모리 제약 환경 (힙 크기 < 4GB)
튜닝 설정
# 기본 설정
java -XX:+UseZGC -XX:+UseGenerationalZGC -Xmx32g MyApplication
# 고급 튜닝
java -XX:+UseZGC \
-XX:+UseGenerationalZGC \
-XX:ZCollectionInterval=0 \
-XX:ZUncommitDelay=300 \
-XX:ZYoungCollectionThreshold=25 \
-Xmx64g \
MyApplication
모니터링 포인트
주요 모니터링 지표:
- 세대별 GC 빈도 (이상적 비율: 신세대:구세대 = 10:1 이상)
- 승격률 (이상적 범위: 5-15%)
- 할당률 (급격한 변화 감지)
- 정지 시간 분포 (99.9% < 10ms 목표)
- 메모리 사용률 (80% 이하 유지 권장)
미래 전망
세대 구분 ZGC는 단순한 성능 개선을 넘어서 GC 기술의 새로운 표준을 제시했습니다:
- 완전한 저지연 달성: 힙 크기와 무관한 1ms 정지 시간, 예측 가능하고 일정한 성능
- 처리량과 지연 시간의 양립: 불가능의 삼각 정리 극복, 94% 처리량 + 1ms 지연 시간 동시 달성
- 클라우드 네이티브 최적화: 컨테이너 환경과 마이크로서비스 아키텍처에 최적화
세대 구분 ZGC는 현대 애플리케이션의 요구사항을 완벽하게 만족하는 차세대 가비지 컬렉터로 자리잡을 것으로 예상됩니다.
JVM 밑바닥까지 파헤치기 - 예스24
“자바 가상 머신의 깊숙한 내부를 향해 떠나는 흥미진진한 모험”C·C++를 사용해 주로 프로그래밍을 하던 시절 까다로운 메모리 관리와 플랫폼 이식성 문제는 개발자들에게 적지 않은 부담이
www.yes24.com