3.6 저지연 가비지 컬렉터
가비지 컬렉터를 평가하는 3가지 핵심 지표가 있어요:
가비지 컬렉터의 불가능한 삼각형
이 세 가지를 모두 완벽하게 만족하는 컬렉터는 만들기 어려워요. 경제학의 '불가능의 삼각 정리'처럼 말이죠!
왜 지연 시간이 가장 중요해졌을까?
하드웨어 발전의 영향
- 메모리 용량: 요즘 서버는 메모리가 넉넉해서 컬렉터가 조금 더 써도 괜찮아요
- CPU 성능: 좋은 하드웨어 = 높은 처리량 = 컬렉터 영향 감소
- 지연 시간: 하지만 이건 달라요! 메모리가 늘어날수록 청소 시간도 늘어나거든요
직관적인 예시
- 1GB 힙 청소 시간 < 1TB 힙 청소 시간 (당연하죠!)
- 그래서 지연 시간이 가장 해결하기 어려운 지표가 되었어요
가비지 컬렉터 진화 과정
컬렉터 역사 한눈에 보기
시리얼 → 패러렐 → CMS → G1 → ZGC/Shenandoah
📍 📍 📍 📍 📍
전체정지 전체정지 부분동시 부분동시 거의동시
각 컬렉터의 동시성 비교
저지연 GC 비교표
ZGC | Shenandoah | 세대 구분 ZGC | |
---|---|---|---|
정지 시간 | 10ms 이하 (힙 크기 무관) | 10ms 이하 | 10ms 이하 + 세대별 최적화 |
최대 힙 크기 | 최대 16TB (JDK 13+) | 제한 없음 (실질적으로 TB급) | 최대 16TB |
동시 이주 | ✅ 지원 | ✅ 지원 | ✅ 지원 |
세대 구분 | ❌ 없음 (단일 세대) | ❌ 없음 (단일 세대) | ✅ Young/Old Gen 구분 |
가용성 | Oracle JDK, OpenJDK | OpenJDK (배포판에 따라 다름) | Oracle JDK, OpenJDK (JDK 21+) |
안정화 버전 | JDK 15+ (프로덕션) | JDK 15+ (프로덕션) | JDK 21+ (아직 발전 중) |
메모리 오버헤드 | 중간 정도 | 약간 더 높음 | 낮음 (세대별 관리로 효율적) |
처리량 | G1GC 대비 약간 낮음 | G1GC 대비 약간 낮음 | ZGC 대비 개선 |
JVM 옵션 비교 | # JDK 15 이전 -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -XX:SoftMaxHeapSize=30g | -XX:+UseShenandoahGC -XX:ShenandoahGCHeuristics=adaptive | # JDK 21+ -XX:+UseZGC -XX:+UseGenerationalZGC |
주요 차이점 | - Oracle과 OpenJDK 모두에서 사용 가능 - 16TB까지 안정적으로 지원 - 힙 크기에 관계없이 일관된 저지연 보장 - 단일 세대 방식으로 구현이 단순함 | - RedHat 주도로 개발 - 일부 Oracle JDK 배포판에서는 제외될 수 있음 - ZGC와 유사한 성능이지만 약간 더 높은 메모리 오버헤드 - 다양한 휴리스틱 옵션 제공 | - 더 나은 성능: 신세대/구세대 구분으로 효율성 향상 - 기본 모드로 격상 예정 |
동시 이주(Concurrent Relocation)의 비밀
기존 방식의 문제
- 객체 찾기 (동시 가능) ✅
2. 객체 이동 (정지 필요) ❌ ← 여기서 병목! (SWT 쩐다!)
3. 참조 업데이트 (정지 필요) ❌
ZGC/Shenandoah의 해법
- 객체 찾기 (동시) ✅
- 객체 이동 (동시!) ✅ ← 혁신!
- 참조 업데이트 (동시!) ✅
컬러드 포인터 (ZGC)
- 64비트 포인터의 일부 비트를 메타데이터로 활용
- 객체 상태를 포인터에 직접 인코딩
- 별도 메타데이터 테이블 불필요!
포워딩 포인터 (Shenandoah)
- 객체 헤더에 새 위치 정보 저장
- 이동 중인 객체에 대한 접근을 투명하게 처리
Shenandoah
Shenandoah는 레드햇(Red Hat)이 독자적으로 개발한 첫 번째 핫스팟 가비지 컬렉터예요. 그런데 여기서 드라마가 시작돼요...
오라클의 견제
- 🚫 JDK 12부터 지원 거부 입장 표명
- 🔒 오라클 JDK에서는 조건부 컴파일로 완전히 제외
- 💸 "유료 상용" 오라클 JDK < "무료 오픈소스" OpenJDK 기능 역전 현상
현재 상황
✅ OpenJDK (Red Hat, Amazon 등): Shenandoah 포함
❌ Oracle JDK: Shenandoah 제외
Shenandoah의 목표
핵심 미션: 힙 크기와 상관없이 GC 정지 시간을 10ms 이내로 제한! 이를 위해서는 표시 단계뿐만 아니라 객체 회수와 마무리 작업까지 사용자 스레드와 동시에 수행해야 했어요.
🤝 G1과의 공통점
Shenandoah는 사실 G1의 좋은 후계자예요:
비슷한 점들:
- 힙을 리전으로 분할 관리
- 거대 리전 (큰 객체) 지원
- 가비지 우선 회수 전략
- 🔄 코드 일부를 직접 공유 (버그 수정, 개선사항 동시 반영)
Shenandoah 덕분에 G1이 받은 혜택:
- 🔧 전체 GC를 멀티스레드로 처리 가능해짐
- 🐛 다양한 버그 수정사항 공유
**
차이점들**
- 동시 이주(Concurrent Evacuation)
-
G1: [표시: 동시] → [이주: 정지] ❌ Shenandoah: [표시: 동시] → [이주: 동시] ✅
-
- 세대 구분 없음 (JDK 21까지)
- G1: 신세대/구세대 구분
- Shenandoah: 모든 리전을 동일하게 처리 (복잡도 vs 성능 트레이드오프)
- 연결 행렬(Connection Matrix)
- 기존 방식 (기억 집합)
- 메모리 많이 사용
- 거짓 공유 문제 발생
- Shenandoah 방식 (연결 행렬):
- 기존 방식 (기억 집합)
이차원 표로 간단하게 리전 간 참조 관리! 💡
Shenandoah의 9단계 동작 과정
전체 프로세스
1️⃣ 최초 표시 [🟥] GC 루트 직접 참조 객체 표시
2️⃣ 동시 표시 [🟦] 객체 그래프 탐색 (사용자와 동시)
3️⃣ 최종 표시 [🟥] 보류 표시 완료 + 회수집합 생성
4️⃣ 동시 청소 [🟦] 완전히 빈 리전들 청소
5️⃣ 동시 이주 [🟦] 🌟 핵심! 객체들을 새 리전으로 복사
6️⃣ 최초 참조 갱신 [🟥] GC/사용자 스레드 집결지 설정
7️⃣ 동시 참조 갱신 [🟦] 옛 주소 → 새 주소 참조 수정
8️⃣ 최종 참조 갱신 [🟥] GC 루트 참조 갱신
9️⃣ 동시 청소 [🟦] 최종 정리 작업
범례: 🟥 = 일시정지, 🟦 = 동시실행
동시 이주 단계
가장 혁신적인 부분이에요! 다른 컬렉터들이 못 한 걸 해냈거든요.
문제상황
GC 스레드: "이 객체를 A 위치에서 B 위치로 옮겨야지!"
사용자 스레드: "어? 나는 이 객체를 지금 쓰고 있는데?"
이런 동시성 문제를 어떻게 해결했을까요?
포워딩 포인터
1984년 로드니 A. 브룩스가 제안한 천재적인 아이디어로 포워딩 포인터를 붙이는 방안이에요
기본 개념
일반 객체: [헤더][필드1][필드2][필드3]
Shenandoah: [포워딩포인터][헤더][필드1][필드2][필드3]
동작 방식
1단계: 평상시
포워딩 포인터 → 자기 자신
[📍] → [객체헤더][데이터]
2단계: 이주 중
옛 객체: [📍] → 새 객체: [헤더][데이터]
[헤더][데이터] (새 위치)
(옛 위치)
사용자가 옛 객체에 접근해도 자동으로 새 객체로 포워딩돼요!
동시성 문제와 해결책
위험한 시나리오
1. GC 스레드: 객체 복사 시작
2. 사용자 스레드: 옛 객체 필드 수정 ← 🚨 문제!
3. GC 스레드: 포워딩 포인터 업데이트
해결책: CAS(Compare-And-Swap) 기법
- 포워딩 포인터 접근 시 동기화
- 한 번에 하나의 스레드만 접근 허용
포워딩 포인터의 비용
장점:
- ✅ 동시 이주 가능
- ✅ 포인터 하나만 수정하면 참조 자동 포워딩
단점:
- ❌ 모든 객체 접근에 추가 우회 비용
- ❌ 읽기/쓰기 장벽 필요 (특히 읽기 장벽이 비쌈)
- ❌ 메모리 오버헤드 (초기에는 5~10% 추가 사용)
계속되는 혁신과 개선
JDK 13: 로드 참조 장벽
이전 문제:
int value = obj.intField;// 읽기 장벽 발동 ❌ (불필요)
Object ref = obj.refField;// 읽기 장벽 발동 ✅ (필요)
개선 후:
- 🎯 참조 타입에만 장벽 적용
- 📈 원시 타입 작업에서 오버헤드 대폭 감소
JDK 13: 포워딩 포인터 객체 헤더 통합
JDK 12까지:
[포워딩포인터][마크워드][클래스워드][배열길이]
JDK 13부터:
[마크워드/포워딩포인터][클래스워드][배열길이]
마크워드의 락 플래그 활용:
- 0b11: 미사용 상태 → 포워딩 포인터로 활용
- 💾 메모리 5~10% 절약
- ⚡ 캐시 적중률 향상
- 📈 벤치마크에서 10~15% 성능 향상
JDK 17: 스택 워터마크
기존 문제:
GC 시작 → 모든 스레드 정지 → 스택 스캔 → 재개
↑ 수십 ms 소요 (병목!)
스택 워터마크 해법:
스택 구조:
┌─────────────────┐ ← 최상위 프레임(변화 있음)
├─ 워터마크 ─────┤
├─────────────────┤ ← 고정 프레임들(변화 없음)
├─────────────────┤(GC가 동시 스캔 가능!)
└─────────────────┘
결과:
- 📊 JDK 11: 421μs → JDK 17: 63μs (최초 표시)
- 📊 JDK 11: 1294μs → JDK 17: 328μs (최종 표시)
📊 실전 성능: 현실은?
🧪 위키백과 200GB 인덱싱 테스트
2015년 초기 버전:
컬렉터 | 실행시간 | 총정지시간 | 최장정지시간 | 평균정지시간 |
---|---|---|---|---|
Shenandoah | 387.6s | 320ms | 89.79ms | 53.01ms |
G1 | 312.0s | 11.75s | 1.24s | 450.12ms |
CMS | 285.2s | 12.78s | 4.39s | 852.26ms |
분석:
- ✅ 정지 시간: 극적으로 단축! (목표 달성)
- ❌ 처리량: 가장 느림 (트레이드오프)
- 🎯 10ms 목표: 아직 미달 (89.79ms)
📈 최신 성능 (OpenJDK 17)
평균 정지 시간:
- 최초 표시: 63μs (0.063ms!)
- 최종 표시: 328μs (0.328ms!)
드디어 1ms 미만 달성! 🎉
🔧 실전 사용 가이드
설정 예시
# 기본 Shenandoah 활성화
-XX:+UseShenandoahGC
-Xmx32g
# 휴리스틱 조정
-XX:ShenandoahGCHeuristics=adaptive# 적응형 (기본)
-XX:ShenandoahGCHeuristics=static# 정적
-XX:ShenandoahGCHeuristics=compact# 컴팩트 우선
# 모니터링
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
주의사항
- 🚫 오라클 JDK에서는 사용 불가
- 💾 메모리 오버헤드 (5~10% 추가 사용)
- ⚡ 읽기/쓰기 장벽으로 인한 처리량 저하
- 🧪 상대적으로 새로운 기술 (안정성 고려 필요)
🎯 결론: Shenandoah의 미래
💪 Shenandoah의 전략: "짧은 보폭으로 빠르게"
레드햇은 처음부터 완벽을 추구하지 않았어요. 대신:
- 🎯 작은 목표들로 분할
- 📈 점진적 개선
- 🔄 꾸준한 업데이트
🌟 인상적인 점들
- 🏢 기업 정치를 이겨낸 기술력
- 오라클의 견제에도 불구하고 꾸준한 발전
- 오픈소스 진영의 승리
- 🧠 혁신적인 아이디어들
- 포워딩 포인터 활용한 동시 이주
- 스택 워터마크 기법
- 연결 행렬로 메모리 효율성 개선
- 📊 실용적인 성과
- 1ms 미만 정지 시간 달성
- JDK 8까지 백포팅으로 광범위한 적용 가능
🤔 선택 가이드
Shenandoah를 고려해보세요:
- OpenJDK 사용 환경
- Red Hat OpenJDK
- Amazon Corretto
- 기타 OpenJDK 배포판
- 극저지연이 핵심 요구사항
- 온라인 게임
- 실시간 거래 시스템
- 스트리밍 플랫폼
- 처리량보다 응답성이 중요
- 새로운 기술 도입에 적극적
다른 컬렉터가 나을 수도:
- Oracle JDK 필수 사용
- 안정성이 최우선
- 처리량 중심 워크로드
- 메모리 사용량에 민감
🚀 ZGC 완전정복: 오라클의 혁신적인 저지연 컬렉터 이야기
안녕하세요! 이번에는 정말 특별한 가비지 컬렉터를 소개해드릴 시간이에요. 바로 ZGC (Z Garbage Collector)!
"Z가 뭐지?" 하시는 분들도 계시겠지만, 이 친구는 정말 충격적인 성능을 보여주는 컬렉터거든요.
🤔 ZGC vs Shenandoah: 같은 목표, 다른 길
📊 공통 목표
둘 다 10ms 이하의 정지 시간을 목표로 하는 저지연 컬렉터예요. 하지만 구현 방식은 완전히 달라요!
Shenandoah: G1의 후계자 스타일
ZGC: Azul의 PGC/C4 컬렉터 계보
🏢 개발 배경의 차이
Shenandoah:
- 🏢 Red Hat 주도 (외부 개발)
- 🚫 Oracle의 견제로 Oracle JDK에서 제외
- 📖 오픈소스 진영의 역작
ZGC:
- 🏛️ Oracle 직접 개발 (내부 프로젝트)
- ✅ JDK 11부터 실험버전, JDK 15부터 정식 지원
- 💰 원래 상용화 계획이었지만 무료로 공개
언젠간 고치겠지... 하면서 정치적 이슈로 갈라선 두 컬렉터가 서로 다른 길을 걸어온 거죠.
🏗️ ZGC의 혁신적인 구조
🧩 동적 리전 관리
기존 컬렉터들과 달리 ZGC는 동적 크기 리전을 사용해요:
G1/Shenandoah: 고정 크기 리전 (예: 16MB)
ZGC: 3가지 동적 크기 리전
- 소리전: 2MB (256KB 미만 객체)
- 중리전: 32MB (256KB~4MB 객체)
- 대리전: 가변 (4MB 이상 객체, 객체당 전용 리전)
대리전의 특별한 점:
- 🎯 하나의 큰 객체 = 하나의 전용 리전
- 🚫 재할당하지 않음 (복사 비용이 너무 크기 때문)
- 📏 최소 4MB, 2MB의 배수 크기
🎨 컬러 포인터: ZGC의 핵심 기술
💡 혁신적인 아이디어
기존 방식:
객체 정보를 어디에 저장할까?
→ 객체 헤더에 추가 필드 생성
ZGC 방식:
포인터 자체에 정보를 저장하자!
→ 컬러 포인터(Colored Pointer) 탄생
🔍 64비트 포인터의 활용
현실적 제약 활용:
이론상 64비트 = 16EB 메모리 (말도 안 되게 큼!)
실제 Linux = 46비트 = 64TB (충분히 큼)
ZGC 제한 = 44비트 = 16TB (여전히 충분)
남는 4비트를 플래그로 활용! 💡
컬러 포인터 구조:
┌─────────────┬──┬──┬──┬──┬─────────────────────────────────┐
│ 사용안함(16) │M1│M0│R │F │ 객체주소(44비트) │
└─────────────┴──┴──┴──┴──┴─────────────────────────────────┘
│ │ │ └─ Finalizable
│ │ └──── Remapped
│ └─────── Marked0
└────────── Marked1
🌟 컬러 포인터의 3대 장점
1️⃣ 즉시 리전 재활용
Shenandoah: 객체이주 → 참조갱신 완료 대기 → 리전 재활용
ZGC: 객체이주 → 즉시 리전 재활용! ✨
빈 리전 하나만 있으면 가비지 컬렉션 완료 가능!
2️⃣ 메모리 장벽 최소화
- 🚫 쓰기 장벽 완전 제거 (세대 구분 없어서 가능)
- 📖 읽기 장벽만 사용 (훨씬 효율적)
- ⚡ 런타임 성능 향상
3️⃣ 확장 가능성
- 📈 상위 16비트 활용 가능 (Linux 미사용 영역)
- 🔮 미래에 힙 제한을 64TB로 확장 가능
- 🧠 추적 정보 등 추가 기능 저장 공간 확보
⚙️ 메모리 다중 매핑의 마법
🤔 문제: 하드웨어는 컬러 포인터를 모른다
프로세서 생각: "0x0000100012345678? 이건 그냥 메모리 주소네!"
ZGC 생각: "이건 Remapped 플래그가 설정된 포인터야!"
💡 해결책: 다중 매핑 기술
핵심 아이디어:
서로 다른 가상 주소 → 같은 물리 메모리 매핑
0x0000000012345678 ─┐
0x0000040012345678 ─┼─→ 같은 물리 메모리
0x0000080012345678 ─┘
0x0000100012345678 ─┘
비유해드릴게요:
"A아파트 102동 1004호"라는 같은 주소가
서울, 부산, 인천에 따라 다른 물리적 위치를 가질 수 있죠?
ZGC도 비슷하게 여러 가상 주소가 같은 물리 메모리를 가리켜요!
🔄 ZGC의 4단계 동작 과정
1단계: 동시 표시 (Concurrent Marking)
🎯 목표: 살아있는 객체들 찾기
🟦 실행: 사용자 스레드와 동시 진행
📊 작업: Marked0/Marked1 플래그 갱신
2단계: 동시 재배치 준비 (Concurrent Relocation Prepare)
🎯 목표: 회수할 리전들을 재배치 집합으로 선정
🟦 실행: 사용자 스레드와 동시 진행
🔍 특징: G1과 달리 모든 리전 스캔!
G1 vs ZGC 비교
G1: 기억 집합 관리 비용 선택 → 점진적 스캔
ZGC: 광범위 스캔 비용 선택 → 기억 집합 불필요
3단계: 동시 재배치 (Concurrent Relocation) ⭐
🎯 목표: 객체들을 새 리전으로 복사
🟦 실행: 사용자 스레드와 동시 진행
✨ 핵심: 포워드 테이블 + 자가 치유
자가 치유(Self-Healing)의 마법:
사용자가 옛 객체 접근 시:
- 읽기 장벽 감지
- 포워드 테이블 확인
- 새 객체로 자동 포워딩
- 참조 자체를 새 주소로 갱신 (한 번만!)
Shenandoah: 접근할 때마다 포워딩 오버헤드
ZGC: 첫 접근 시에만 오버헤드! 🎉
4단계: 동시 재매핑 (Concurrent Remapping)
🎯 목표: 힙 전체의 낡은 참조들 정리
🟦 실행: 사용자 스레드와 동시 진행
🧠 영리함: 다음 표시 단계와 통합! (객체 그래프 탐색 1회 절약)
💪 ZGC vs 다른 컬렉터들
절충점 비교
컬렉터 | 장점 | 단점 |
---|---|---|
G1 | 기억 집합으로 점진적 회수 | 메모리 오버헤드 + 쓰기 장벽 |
Shenandoah | 동시 이주 지원 | 읽기/쓰기 장벽 모두 사용 |
ZGC | 쓰기 장벽 완전 제거 | 객체 할당 속도 제한 |
⚠️ ZGC의 약점: 할당 속도 제한
문제 시나리오:
- ZGC가 거대한 힙에서 10분간 동시 회수 시작
- 애플리케이션이 매우 빠르게 새 객체 생성
- 회수 속도 < 할당 속도
- 부유 쓰레기 대량 발생
- 힙 공간 고갈 위험! 😱
현재 해결책:
- 🏗️ 힙 크기를 최대한 크게 설정
- 🔮 세대 구분 ZGC 도입 (JDK 21+)
🌐 NUMA 메모리 최적화
NUMA란?
- 🖥️ 멀티코어 프로세서에서 각 코어가 메모리 직접 관리
- 📍 로컬 메모리 접근 = 빠름
- 🌐 원격 메모리 접근 = 느림
ZGC의 영리함:
객체 생성 요청한 스레드의 프로세서 → 해당 프로세서의 로컬 메모리에 우선 할당!
→ 메모리 접근 효율 극대화 ⚡
🔧 ZGC 설정 가이드
기본 설정
# JDK 15+ (정식 지원)-XX:+UseZGC
-Xmx32g
# JDK 11~14 (실험 버전)-XX:+UnlockExperimentalVMOptions
-XX:+UseZGC
고급 설정
# 소프트 힙 제한 (추천)-XX:SoftMaxHeapSize=30g
# NUMA 최적화-XX:+UseTransparentHugePages
# 모니터링-XX:+PrintGC
-XX:+PrintGCDetails
주의사항
# 이런 옵션들과 호환 안됨
-XX:+UseCompressedOops# 압축 포인터 불가
-XX:+Use32BitClassPointers# 32비트 클래스 포인터 불가
ZGC 선택 가이드
ZGC가 최적인 경우
- 🎯 극저지연이 핵심 요구사항
- 실시간 거래 시스템
- 온라인 게임 서버
- 실시간 스트리밍
- 📊 대용량 힙 사용
- 8GB+ 메모리 환경
- 빅데이터 처리
- 대규모 인메모리 캐시
- 64비트 플랫폼
- Linux, macOS, Windows (64비트만)
- 16TB 이하 힙 크기 (충분히 큼!)
다른 컬렉터 고려할 경우
- 📱 32비트 환경 → 불가능
- 🔧 압축 포인터 필수 → G1 추천
- 💾 메모리 극도로 제한적 → G1/Parallel 고려
- 🎯 처리량 최우선 → Parallel 여전히 유효
🔥 세대 구분 ZGC: 차세대 가비지 컬렉터의 완성형
이번에는 ZGC의 진화된 형태인 세대 구분 ZGC에 대해 이야기해볼게요.
"ZGC도 충분히 좋은데 뭐가 더 필요해?" 하실 수도 있지만, 이 친구는 정말 게임 체인저예요!
🤔 왜 세대 구분이 필요했을까?
🎯 기본 ZGC의 한계
문제 상황:
모든 객체를 함께 보관 → 매번 전체 힙 스캔 필요
- 젊은 객체 (금방 죽을 예정) 💀
- 늙은 객체 (오래 살 예정) 👴
→ 비효율적인 작업!
약한 세대 가설의 활용:
젊은 객체는 일찍 죽는다!
→ 젊은 객체만 더 자주 회수하면
→ 적은 노력으로 많은 메모리 확보 가능! 💡
🚀 JDK 21의 세대 구분 ZGC
🔧 활성화 방법
# 기본 ZGC (여전히 세대 구분 없음)
java -XX:+UseZGC MyApp
# 세대 구분 ZGC 활성화
java -XX:+UseZGC -XX:+ZGenerational MyApp
미래 계획:
현재: -XX:+UseZGC = 기본 ZGC
미래: -XX:+UseZGC = 세대 구분 ZGC (기본)
-XX:-ZGenerational = 세대 구분 비활성화
최종: 세대 구분 ZGC만 남길 예정
🎨 향상된 컬러 포인터
기존 ZGC:
- 📖 읽기 장벽만 사용
- 🚫 쓰기 장벽 없음
세대 구분 ZGC:
읽기 장벽 + 쓰기 장벽 조합
- 컬러 포인터에 새로운 메타데이터 추가
- 쓰기 장벽이 세대 간 참조 효율적 추적
- 읽기 장벽 작업을 쓰기 장벽으로 일부 이전
(읽기 >> 쓰기 빈도이므로 전체 작업량 감소!)
🛠️ 핵심 개선 기술들
1️⃣ 다중 매핑 메모리 제거
기존 문제:
ZGC는 같은 힙을 3개 가상 주소로 매핑
→ ps 명령어로 보면 실제의 3배 메모리 사용으로 표시 😅
→ 혼란 야기
세대 구분 ZGC 해결:
- ✅ 읽기/쓰기 장벽 명확히 구분 → 다중 매핑 불필요
- 📊 사용자 관점에서 정확한 메모리 사용량 측정 가능
- 🎯 확보된 메타데이터 비트로 힙 크기 16TB+ 확장 가능
2️⃣ 이중 버퍼 기억 집합 관리
기존 카드 테이블 방식:
카드 테이블: 1바이트 = 힙의 512바이트 대응
문제: 더럽혀진 512바이트 전체를 스캔해야 함 (비효율)
세대 구분 ZGC 비트맵 방식:
비트맵: 1비트 = 객체 필드 주소 1개 (정확!)
이중 버퍼: [애플리케이션용] ↔ [GC 전용] 원자적 교환
→ 서로 간섭 없이 동시 작업 가능! 🎯
3️⃣ 밀집도 기반 리전 처리
영리한 전략:
최근 할당 리전 → 살아있는 객체 많음 (밀집도 높음)
오래된 리전 → 죽은 객체 많음 (밀집도 낮음)
처리 방식:
1. 밀집도 낮은 리전 우선 회수 📊
2. 밀집도 높은 리전은 자연스럽게 노화 👴
3. 노화된 리전은 다음 GC에서 밀집도 낮아짐
4. 효율적인 순환 구조 완성!
4️⃣ 거대 객체의 스마트 처리
기존 문제:
거대 객체를 구세대로 이동? → 복사 비용 엄청남 💸
세대 구분 ZGC 해결:
1. 거대 객체를 신세대에 할당
2. 금방 죽으면 → 신세대 GC에서 빠르게 회수 ⚡
3. 오래 살면 → 리전 자체가 구세대로 승격 (복사 없음!)
📊 압도적인 성능 향상
🚀 Apache Cassandra 벤치마크
기본 ZGC 대비 세대 구분 ZGC:
처리량 4배 향상! 🎉
"NoSQL 데이터베이스에서 이 정도 차이라니!"
💾 메모리 효율성 개선
- 🔄 다중 매핑 제거로 메모리 오버헤드 감소
- 📈 더 정확한 메모리 사용량 측정
- 🎯 전체 자원 사용량 최적화
⚡ 다양한 장벽 최적화
컴파일된 애플리케이션 코드에 더 많은 GC 코드 통합
→ 기억 집합 장벽, 시작 단계 스냅숏 표시 장벽,
쓰기 장벽 버퍼, 장벽 패치 등 수많은 최적화 기법 적용
→ 처리량 극대화!
🔄 동작 방식의 진화
📋 기본 ZGC vs 세대 구분 ZGC
기본 ZGC:
1. 동시 표시 (전체 힙)
2. 동시 재배치 준비 (전체 힙)
3. 동시 재배치 (선택된 리전)
4. 동시 재매핑 (전체 힙)
세대 구분 ZGC:
신세대 GC (자주):
1. 신세대 표시
2. 밀집도 분석 → 회수 대상 선정
3. 동시 재배치 (효율성 극대화)
4. 생존 객체 → 생존자/구세대 승격
구세대 GC (가끔):
1. 전체 표시 (필요시에만)
2. 구세대 회수
🎯 실전 사용 가이드
설정 예시
# 세대 구분 ZGC 기본 설정-XX:+UseZGC
-XX:+ZGenerational
-Xmx32g
# 고급 설정-XX:+UseZGC
-XX:+ZGenerational
-XX:SoftMaxHeapSize=30g
-XX:+UseLargePages
# 모니터링 강화-XX:+PrintGC
-XX:+PrintGCDetails
-Xlog:gc*:gc.log
✅ 세대 구분 ZGC가 최적인 경우
- 📊 객체 생성이 매우 빈번한 애플리케이션
- 웹 서버 (요청당 많은 임시 객체)
- 스트리밍 처리 (지속적인 데이터 흐름)
- 실시간 분석 시스템
- 🎯 기본 ZGC에서 할당 속도 제한 경험한 경우
- 회수 속도 < 할당 속도 상황
- 힙 크기를 계속 늘려야 했던 경우
- 💾 메모리 사용량 최적화가 중요한 환경
- 컨테이너 환경
- 메모리 제한이 엄격한 서버
🤔 아직 기본 ZGC 고려할 경우
- 🧪 검증된 안정성 우선
- 미션 크리티컬한 서비스
- 새로운 기능 도입에 보수적인 환경
- 📈 객체 수명이 비슷한 워크로드
- 배치 처리 시스템
- 장시간 실행되는 계산 작업
3.7 적합한 가비지 컬렉터 선택하기
지금까지 다양한 가비지 컬렉터들을 살펴봤는데, 이제 가장 중요한 질문이 남았어요.
"그래서 결국 뭘 써야 하는데?"
이번에는 실전에서 어떤 컬렉터를 선택해야 하는지, 그리고 정말 특별한 컬렉터 하나까지 소개해드릴게요!
🎯 엡실론 컬렉터: 아무것도 안 하는 컬렉터?
😲 충격적인 컨셉
JDK 11에 엡실론(Epsilon) 컬렉터라는 정말 특별한 친구가 추가되었어요.
엡실론 컬렉터의 특징:
- 가비지 컬렉션을 전혀 하지 않음!
- "No-Op Garbage Collector"
- 일명 "일하지 않는" 컬렉터
"말도 안 된다고?" 하시는 분들, 잠깐만요! 이 친구도 나름의 이유가 있어요.
🛠️ 엡실론의 진짜 역할
가비지 컬렉터 ≠ 단순한 쓰레기 수거꾼
실제로는 "자동 메모리 관리 서브시스템"이에요:
- 힙 관리 및 레이아웃
- 객체 할당 처리
- 인터프리터/컴파일러 연동
- 모니터링 시스템 연동
엡실론도 최소한 힙 관리와 객체 할당은 해야 JVM이 돌아가거든요.
🎪 엡실론의 실제 용도
1️⃣ 성능 테스트 및 벤치마킹
"GC의 영향을 완전히 배제하고 싶어!"
→ 엡실론으로 순수 애플리케이션 성능 측정
2️⃣ 통합 인터페이스 검증
JDK 10부터 가비지 컬렉터 통합 인터페이스 도입
→ 엡실론이 이 인터페이스의 참조 구현 역할
3️⃣ 마이크로서비스/서버리스 환경
전통 자바의 약점:
- 메모리 많이 사용
- 구동 시간 오래 걸림
- JIT 최적화 지연
단기 실행 서비스:
- 몇 초~몇 분만 실행
- 힙이 가득 차기 전에 종료
- GC 부하 제거로 빠른 실행!
📊 가비지 컬렉터 선택 매트릭스
🎯 워크로드별 최적 선택
워크로드 유형 추천 컬렉터 이유
마이크로서비스 Epsilon, G1 짧은 실행시간, 빠른 시작
웹 애플리케이션 | G1, ZGC | 응답시간 중요, 중간 크기 힙 |
---|---|---|
실시간 거래 | ZGC, Shenandoah | 극저지연 필요 |
빅데이터 처리 | Parallel, ZGC | 처리량 또는 대용량 힙 |
배치 작업 | Parallel | 처리량 최우선 |
게임 서버 | ZGC, G1 | 일정한 응답시간 |
💾 힙 크기별 가이드
< 1GB: G1, Parallel
1GB~8GB: G1 (안정적 선택)
8GB~32GB: G1, ZGC (성능 테스트 후 결정)
32GB+: ZGC, Shenandoah (저지연 우선)
⏱️ 지연시간 요구사항별
수 초 OK: Parallel (처리량 최우선)
수백 ms OK: G1 (균형잡힌 선택)
수십 ms 필요: G1 튜닝, ZGC 고려
10ms 이하: ZGC, Shenandoah
1ms 이하: ZGC (세대 구분), Shenandoah
🔍 실전 선택 프로세스
1단계: 기본 조건 확인
# 플랫폼 체크
- 32비트? → Serial, Parallel만 가능
- 64비트? → 모든 컬렉터 선택 가능
# JDK 버전 체크
- JDK 8: G1, CMS, Parallel
- JDK 11+: G1, ZGC, Shenandoah, Epsilon
- JDK 21+: 세대 구분 ZGC 추가
2단계: 비즈니스 요구사항 분석
핵심 질문들:
- 최대 힙 크기는?
- 허용 가능한 최대 정지시간은?
- 처리량과 지연시간 중 뭐가 더 중요한가?
- 메모리 사용량에 제약이 있나?
3단계: 초기 선택
보수적 선택: G1 (대부분 상황에서 무난)
공격적 선택: ZGC (최신 기술, 최고 성능)
특수 목적: Epsilon (단기 실행), Parallel (배치)
4단계: 성능 테스트
# 기본 설정으로 테스트-XX:+UseG1GC
-XX:+UseZGC -XX:+ZGenerational
-XX:+UseParallelGC
# 지표 측정- 처리량 (TPS, QPS)
- 평균/p99 응답시간
- GC 빈도 및 정지시간
- 메모리 사용량
⚙️ 컬렉터별 기본 설정 가이드
G1GC (안정적 선택)
# 기본 설정
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
# 튜닝 옵션
-XX:G1NewSizePercent=20
-XX:G1MaxNewSizePercent=40
ZGC (고성능 선택)
# 세대 구분 ZGC (JDK 21+)-XX:+UseZGC
-XX:+ZGenerational
-XX:SoftMaxHeapSize=30g
# 기본 ZGC-XX:+UseZGC
-Xmx32g
Parallel GC (처리량 우선)
# 기본 설정
-XX:+UseParallelGC
-XX:ParallelGCThreads=8
# 튜닝
-XX:MaxGCPauseMillis=500
-XX:GCTimeRatio=99
Epsilon (특수 용도)
# 활성화-XX:+UnlockExperimentalVMOptions
-XX:+UseEpsilonGC
# 메모리 모니터링 필수-Xmx1g # 충분한 힙 크기 설정
📈 모니터링과 튜닝
핵심 지표
# GC 로깅 활성화-Xlog:gc*:gc.log:time
# 모니터링 도구- JVisualVM
- GCEasy.io (로그 분석)
- Prometheus + Grafana
- APM 도구 (New Relic, DataDog 등)
문제 해결 체크리스트
1. GC 빈도가 너무 높다면?
- 힙 크기 증가 고려
- 객체 풀링 도입
- 메모리 누수 확인
2. 정지 시간이 너무 길다면?
- 저지연 컬렉터로 변경 (ZGC, Shenandoah)
- 힙 크기 조정
- GC 파라미터 튜닝
3. 처리량이 부족하다면?
- Parallel GC 고려
- JIT 컴파일 최적화
- 하드웨어 리소스 확인
🎯 추천 선택 전략
🥇 2024년 추천 순위
1순위: G1GC
✅ 검증된 안정성
✅ 대부분 상황에서 우수한 성능
✅ 풍부한 튜닝 옵션
✅ 광범위한 호환성
2순위: ZGC (세대 구분)
✅ 최고 수준의 지연시간
✅ 뛰어난 처리량
✅ 대용량 힙 지원
⚠️ 상대적으로 새로운 기술
3순위: Parallel GC
✅ 최고의 처리량
✅ 간단한 설정
❌ 높은 정지시간
🚀 미래 지향적 선택
현재 검증된 선택: G1GC
미래를 준비한 선택: ZGC (세대 구분)
특수 목적: Epsilon, Shenandoah
레거시 지원: Parallel GC
가비지 컬렉터 선택에 정답은 없어요. 각자의 상황과 요구사항에 따라 최적해가 달라지거든요.
핵심 원칙:
- 🧪 실제 워크로드로 테스트하기
- 📊 지표 기반으로 판단하기
- 🔄 지속적으로 모니터링하기
- ⚖️ 트레이드오프 이해하기
엡실론 컬렉터처럼, 때로는 "아무것도 안 하는" 것이 정답일 수도 있어요. 마이크로서비스 시대에는 더욱 그렇죠.
컬렉터 선택 기준
3가지 주요 요인으로 결정:
- 애플리케이션 목적: 데이터 분석(처리량 우선) vs SLA 서비스(지연시간 우선) vs 클라이언트 앱(메모리 사용량)
- 하드웨어 환경: 아키텍처, 프로세서 수, 메모리 용량, OS
- JDK 제공자: Oracle, OpenJDK, Zulu 등
실전 선택 가이드
브라우저-서버 시스템 예시:
- 자금 여유 + 경험 부족 → Azul C4 (상용)
- 제어 능력 있음 → ZGC 시도
- 레거시 시스템 → 4-6GB 이하면 CMS, 그 이상이면 G1
Oracle 공식 가이드:
- ~100MB 데이터 → Serial
- 단일 프로세서 → Serial
- 최대 성능 우선 → Parallel
- 응답시간 중요 → G1
- 매우 빠른 응답 필요 → ZGC
JDK 9 통합 로깅
- Xlog 매개변수 구조:
-Xlog:[selector]:[output]:[decorators]:[output-options]
주요 GC 로그 옵션:
- 기본: -Xlog:gc (구 -XX:+PrintGC)
- 상세: -Xlog:gc* (구 -XX:+PrintGCDetails)
- 힙 정보: -Xlog:gc+heap=debug
- 정지시간: -Xlog:safepoint
로그 레벨: Trace > Debug > Info(기본) > Warning > Error > Off
GC와 관련하여 간략하게 표로 정리한 것들입니다. 아래 표에서 언급되는 가비지 컬렉터는 핫스팟(HotSpot) JVM에서 사용되는 것들입니다.
JDK 버전JVM기본 가비지 컬렉터주요 다른 가비지 컬렉터 / 상태
JDK 1.3 이하 HotSpot JVM 시리얼 컬렉터 (Serial Collector) JDK 1.3.1까지 유일한 선택
JDK 1.4 | 시리얼 컬렉터 (Serial Collector) | 패러럴 스캐빈지 (Parallel Scavenge), 패러럴 올드 (Parallel Old) - 사용 가능 |
---|---|---|
JDK 5 | 패러럴 컬렉터 (Parallel Collector) | 시리얼 컬렉터 (Serial Collector), CMS 컬렉터 (CMS Collector) - 사용 가능 |
JDK 6 | 패러럴 컬렉터 (Parallel Collector) | CMS 컬렉터 (CMS Collector) - 최적화 및 지원 강화. 시리얼 컬렉터 (Serial Collector) - 사용 가능. |
JDK 7 | 패러럴 컬렉터 (Parallel Collector) | G1 컬렉터 (G1 Collector) - 초기 접근(early access) 버전으로 도입. CMS 컬렉터 (CMS Collector), 시리얼 컬렉터 (Serial Collector) - 사용 가능. |
JDK 8 | 패러럴 컬렉터 (Parallel Collector) | G1 컬렉터 (G1 Collector) - 중요한 역할. CMS 컬렉터 (CMS Collector), 시리얼 컬렉터 (Serial Collector) - 사용 가능. |
JDK 9 | G1 컬렉터 (G1 Collector) | CMS 컬렉터 (CMS Collector) - Deprecated(더 이상 사용되지 않음). 시리얼 컬렉터 (Serial Collector), 패러럴 컬렉터 (Parallel Collector) - 사용 가능. |
JDK 11 | G1 컬렉터 (G1 Collector) | ZGC, 쉐년도어 (Shenandoah) - 실험적(experimental)으로 도입. 시리얼 컬렉터 (Serial Collector), 패러럴 컬렉터 (Parallel Collector) - 사용 가능. |
JDK 12 | G1 컬렉터 (G1 컬렉터) | 쉐년도어 (Shenandoah) - 도입. ZGC - 실험적. 시리얼 컬렉터 (Serial Collector), 패러럴 컬렉터 (Parallel Collector) - 사용 가능. |
JDK 14 | G1 컬렉터 (G1 컬렉터) | 패러럴 올드 GC (Parallel Old GC) - Deprecated(더 이상 사용되지 않음). ZGC, 쉐넌도어 - 사용 가능. |
JDK 15 | G1 컬렉터 (G1 컬렉터) | ZGC, 쉐년도어 (Shenandoah) - 프로덕션 레디(production-ready)로 전환. |
JDK 17 | G1 컬렉터 (G1 컬렉터) | ZGC, 쉐넌도어 (Shenandoah) - 상당한 성능 향상. |
JDK 21 | G1 컬렉터 (G1 컬렉터) | 세대 구분 ZGC (Generational ZGC) - 도입. ZGC, 쉐넌도어 - 사용 가능. |
- 기본 가비지 컬렉터:
- JDK 5부터 JDK 8까지는 패러럴 컬렉터가 기본으로 제공되었습니다.
- JDK 9부터는 G1 컬렉터가 기본 가비지 컬렉터로 지정되었으며, 현재(JDK 21까지)까지 유지되고 있습니다.