F-LAB JAVA · 2주차 · Phase 4 · G1 GC 심화
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
-XX:MaxGCPauseMillis=200 옵션의 정확한 의미는?G1 GC는 "큰 힙 시대의 응답"이다.
2GB 시절 GC가 그대로 32GB에 적용되면 STW가 분 단위로 폭발한다.
G1은 힙을 여러 작은 리전(Region) 으로 분할하고, 정지시간 한도 안에서 회수 효과가 큰 리전부터 처리하는 새 접근을 도입.
이로써 "M밀리초 안에 끝낼게" 라는 SLA를 약속할 수 있게 됐다.
| 시대 | 비유 |
|---|---|
| 소형 도서관 (서가 1~2개) | Serial GC — 혼자 한 번에 청소. 5분 |
| 중형 도서관 (서가 10개) | Parallel GC — 여러 사람이 동시 청소. 1분 |
| 대형 도서관 (서가 1000개) | CMS — 청소 중에도 손님이 책 빌릴 수 있게 |
| 거대 도서관 (서가 10000개) | G1 — "오늘 30분 청소 가능. 가장 더러운 서가부터" |
도서관이 작을 땐 단순한 전략 OK.
커질수록 "전부 다 청소"가 비현실적 → 우선순위 + 시간 약속.
1. 시대적 배경 — 메모리가 점점 커지고 있다
2. Serial GC — 1세대의 단순함과 한계
3. Parallel GC — 멀티코어의 활용
4. CMS GC — 동시 마킹의 시도
5. 큰 힙의 저주 — STW 시간 폭발
6. G1 GC의 등장 — 새로운 접근
7. 정지시간 예측 모델
8. ILIC 실무 — GC 선택 가이드
9. 면접 질문 + 자기 점검
1995년 (Java 1.0):
- 단일 코어 CPU
- RAM: 16~64 MB
- JVM Heap: 수 MB
→ Serial GC로 충분
2000년대 초:
- 2~4코어 CPU
- RAM: 1~4 GB
- JVM Heap: 256MB ~ 1GB
→ Parallel GC 등장
2010년대:
- 8~16코어 CPU
- RAM: 32~64 GB
- JVM Heap: 4~16 GB
→ CMS, G1 필요
2020년대 (현재):
- 32~128코어 CPU
- RAM: 256GB+
- JVM Heap: 32~256 GB
→ G1, ZGC, Shenandoah
→ 하드웨어는 100배 커졌는데, GC가 그대로면? → 재앙.
ILIC 운영 서버 (가정):
- Spring Boot 앱
- JVM Heap: 4 GB
- 평균 객체 수: 수백만 개
- 응답 SLA: P99 < 500ms
만약 GC가 1초씩 STW 한다면:
→ GC 시간 통제가 운영 안정성의 핵심.
1. 처리량 (Throughput)
- 단위 시간당 처리할 수 있는 요청 수
- GC 시간이 적을수록 ↑
2. 응답 시간 (Latency)
- 개별 요청의 응답 시간
- STW 짧을수록 ↑
3. 메모리 효율 (Footprint)
- 같은 작업에 쓰는 메모리
- GC 자체의 메모리 오버헤드
4. 시간 예측 가능성 (Predictability)
- "최악의 경우 얼마나 멈출까?"
- SLA 보장
5. 큰 힙 처리 (Large Heap)
- 32GB+ 힙에서도 동작
각 GC는 이 5가지를 다른 비율로 만족.
Serial GC:
- 단일 스레드로 GC 수행
- 모든 애플리케이션 스레드 정지 (STW)
- 마킹 → 청소 → (압축)
- 단순하지만 멀티코어 활용 못 함
java -XX:+UseSerialGC App
1명의 청소부가 사무실 청소:
모든 직원 퇴근 → 청소부 혼자 작업 → 다음 날 출근
사무실이 작으면 OK
사무실이 크면 청소 시간 길어짐 → 직원 못 출근
ILIC 서버:
- 16코어 CPU
- 4GB Heap
- 동시 처리 요청 다수
Serial GC 사용 시:
- 1코어만 GC 사용 → 나머지 15코어 놀음
- GC 시간 매우 김
- 운영 안 됨
→ 운영 서버에선 절대 사용 X.
면접 단골:
"Serial GC와 Parallel GC 차이는?"
→ Serial은 단일 스레드, Parallel은 멀티 스레드 GC.
→ 둘 다 STW 발생.
기초 개념. 다른 GC를 이해하는 출발점.
Parallel GC (Throughput GC):
- 여러 스레드로 GC 수행
- 여전히 STW (모든 앱 스레드 정지)
- GC 시간 = Serial / N (CPU 코어 수)
- 처리량 우선
java -XX:+UseParallelGC App # Java 8 기본
java -XX:+UseParallelOldGC App # Old gen도 Parallel
청소부 16명이 사무실 청소:
여전히 모든 직원 퇴근해야 함
하지만 청소 시간이 1/16로 줄어듦
처리량 = 애플리케이션 시간 / 전체 시간
만약 1시간 중:
- GC: 1분 (1.7%)
- 앱: 59분
처리량 = 98.3%
목표: 처리량 최대화
→ GC 시간을 줄이는 게 핵심
Parallel GC는 이 목표에 최적.
Heap 4GB:
Parallel GC Full GC 시간: ~400ms (8코어 기준)
Heap 16GB:
Full GC 시간: ~1.5초
Heap 32GB:
Full GC 시간: ~3초+
→ 힙 크기에 비례해서 STW 증가.
ILIC 응답 SLA: P99 < 500ms
Parallel GC + 4GB Heap:
보통: 20ms
Full GC 시: 400ms ← 가끔 SLA 근접
GC 폭발 시: 1초+ ← SLA 위반
→ 처리량은 좋지만, 응답 시간 안정성 부족.
CMS (Concurrent Mark Sweep):
- 4단계:
1. Initial Mark (STW, 짧음)
2. Concurrent Mark (앱과 동시)
3. Remark (STW, 짧음)
4. Concurrent Sweep (앱과 동시)
- 대부분의 GC 작업을 앱과 동시 수행
- STW 시간 단축
java -XX:+UseConcMarkSweepGC App
청소부가 손님 받는 도중에 청소:
- 출입구 입장 통제 (STW 1초)
- 청소 (손님 받으면서)
- 정리 정돈 (STW 30초)
- 다시 청소 (손님 받으면서)
→ 도서관 닫는 시간 짧음
→ 하지만 청소가 비효율적
1. STW 시간 짧음 (Initial Mark + Remark만)
2. 응답 시간 안정
3. 인터랙티브 서비스에 적합
CMS는 결국 Java 9에서 deprecated, Java 14에서 완전 제거됨.
이유:
앱이 너무 빨리 객체 생성:
Concurrent Sweep 중에 Old gen 가득 참
→ Full GC 발동 (Serial!)
→ 매우 긴 STW (10초+)
→ "Concurrent Mode Failure" 로그
CMS는 Sweep만 함 (Compaction 안 함)
→ Old gen에 작은 빈 공간들 누적
→ 큰 객체 할당 시 공간 부족
→ Full GC 발동
Concurrent 작업이 앱 스레드와 동시 실행
→ 앱 CPU 사용량 감소 (10~20%)
→ 처리량 손해
"이번 GC가 언제 끝날까?" 알 수 없음
→ SLA 약속 어려움
→ G1이 이 모든 문제를 해결하려고 등장.
CMS의 기여:
- "Concurrent" 개념 도입
- STW 시간 단축의 가능성 증명
- G1 / ZGC / Shenandoah의 영감
CMS의 종말:
- 단편화 문제 해결 못 함
- 큰 힙에서 한계
- G1으로 대체
기존 GC들의 공통점:
GC 한 번 = 영역 전체를 한꺼번에 처리
- Minor GC: Young Gen 전체
- Major GC: Old Gen 전체
- Full GC: 모든 영역
힙이 클수록:
- 마킹할 객체 많음
- 청소할 메모리 많음
- 복사할 데이터 많음
→ STW 시간 비례 증가
가상 측정 (Parallel GC 기준):
| Heap 크기 | Minor GC 평균 | Full GC 평균 |
|---|---|---|
| 1 GB | 20 ms | 200 ms |
| 4 GB | 80 ms | 800 ms |
| 16 GB | 300 ms | 3 초 |
| 32 GB | 600 ms | 6 초+ |
| 64 GB | 1.2 초 | 12 초+ |
→ 선형 증가. 32GB 힙 = 6초 STW.
SLA P99 < 500ms 인 서비스:
Heap 1GB: ✓ Full GC 가끔이라면 OK
Heap 4GB: △ Full GC 시 SLA 위반
Heap 16GB: ❌ 매번 Full GC가 SLA 위반
Heap 32GB+: ❌❌ 실시간 서비스 불가능
그러면 작은 힙만 쓰면 되지 않나? 그게 안 되는 이유:
ILIC 같은 서비스:
- 캐시: Redis 못 쓸 때 in-memory cache
- 대용량 데이터 처리: 1만 건 화물 배치
- 사용자 세션 보관
- 풀링 (Connection, Thread)
- 다양한 라이브러리들의 메모리 (수백 MB)
→ 4GB 이하로 운영 어려움
→ 8~16GB 권장
2010년대:
- 클라우드 시대
- 마이크로서비스
- 큰 힙 + 짧은 STW 동시에 필요
- 기존 GC로 불가능
해결책:
- GC 알고리즘의 근본적 변화 필요
→ G1 GC 등장
기존 GC: "Young/Old 전체를 한 번에"
G1: "힙을 작은 리전으로 나누고, 우선순위로 일부만"
기존 GC:
┌─────── Young ────────┬───── Old ──────┐
│ Eden | Surv | Surv │ │
└─────────────────────┴──────────────────┘
영역 크기와 위치 고정
G1 GC:
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│E│ │O│ │S│O│ │O│E│ │ │S│ │O│E│ │O│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
같은 크기의 리전들. 역할은 동적 결정.
→ Unit 4.3에서 정밀히. 이번엔 개요만.
기존 GC: Full GC = 전체 처리 = 긴 STW
G1: 매번 일부 리전만 처리 = 짧은 STW의 누적
비유:
기존: 매년 한 번 대청소 (집 전체, 1주일)
G1: 매주 일부 방 청소 (1방, 1시간)
"이번 GC는 200ms 안에 끝낼게"
G1이 어떻게 약속을 지키나?
1. 각 리전의 회수 시간 학습 (이전 기록)
2. 시간 한도 안에 처리 가능한 리전만 선택
3. 시간 부족하면 일부 리전 다음 사이클로
# G1 활성화 (Java 9+ 기본)
java -XX:+UseG1GC App
# 정지시간 목표
java -XX:MaxGCPauseMillis=200 App # 기본 200ms
# 리전 크기
java -XX:G1HeapRegionSize=16m App # 기본 자동
# 힙 크기
java -Xms4g -Xmx4g App
Java 8: Parallel GC 기본
Java 9+: G1 GC 기본 ★
Java 17+: G1 GC 기본, ZGC/Shenandoah 옵션
Java 21+: Generational ZGC 추가
→ 박승제씨가 Java 17 환경이면 이미 G1 사용 중.
G1의 이름의 유래:
Garbage First
의미:
"쓰레기가 가장 많은 리전부터 회수"
즉:
- 모든 리전을 다 회수하지 않음
- 회수 효과(여유 공간 / 시간) 가장 큰 리전 선택
- 시간 한도 안에서 최대 효과
→ Unit 4.5에서 상세. 우선순위 회수.
사용자: "GC 200ms 안에 끝내줘"
G1: "알겠어. 가능한 만큼만 할게"
내부 동작:
1. 과거 데이터: "리전 1개 처리에 평균 20ms 걸렸음"
2. 시간 한도 200ms → 약 10개 리전 처리 가능
3. 가장 효과 큰 10개 리전 선택
4. 그것만 처리 → 약 200ms 완료
5. 나머지 리전은 다음 사이클로
정지시간 예측 모델은 어떻게 시간을 통제하는가?
답:
1. 각 리전의 회수 비용을 과거 데이터로 추정
2. 시간 한도 안에 처리 가능한 리전 수 계산
3. 회수 효과 큰 리전을 우선 선택
4. 시간 부족하면 일부는 다음 사이클로 연기
→ 시간을 약속할 수 있는 GC.
G1도 항상 200ms 안에 끝내는 건 아님:
약속이 깨지는 경우:
1. 너무 큰 객체 처리 (거대 리전, Unit 4.4)
2. Old gen 가득 차서 Full GC 발동
3. 메타데이터 부족 (Metaspace)
4. 매우 큰 root set (수십만 스레드)
→ 200ms는 목표, 보장은 아님
→ 평소엔 거의 지킴 (90~95%)
더 엄격한 SLA는 Java 11+의 ZGC, Shenandoah:
G1: 목표 < 200ms, 평균 50~100ms
ZGC: 목표 < 10ms (대부분 달성)
Shenandoah: 목표 < 10ms (대부분 달성)
ZGC/Shenandoah는:
→ ILIC가 P99 50ms 같은 매우 엄격한 SLA면 ZGC 검토.
→ 일반적인 SLA(P99 500ms)는 G1으로 충분.
# GC 로그 활성화 (Java 11+)
java -Xlog:gc*:file=gc.log -jar app.jar
# 로그 예:
# [0.123s][info][gc] GC(0) Pause Young (G1 Evacuation Pause) 50M->10M(256M) 15.234ms
# ↑ 실제 STW 시간
# 분석 도구: GCViewer, GCEasy
운영에서 GC 로그 모니터링은 필수.
"평균 50ms, P99 150ms" 같은 통계 확인.
┌──────────────────────────────────────────────────┐
│ ILIC 같은 서비스의 GC 선택 │
├──────────────────────────────────────────────────┤
│ Heap < 1GB, 단순 batch: │
│ → Serial GC │
├──────────────────────────────────────────────────┤
│ Heap 1~4GB, 처리량 우선: │
│ → Parallel GC │
├──────────────────────────────────────────────────┤
│ Heap 1~32GB, 응답시간 우선: │
│ → ★ G1 GC (가장 권장) │
├──────────────────────────────────────────────────┤
│ Heap 8GB+, 매우 엄격한 SLA (P99 < 50ms): │
│ → ZGC 또는 Shenandoah │
├──────────────────────────────────────────────────┤
│ Heap 256GB+: │
│ → ZGC (큰 힙 전문) │
└──────────────────────────────────────────────────┘
ILIC Spring Boot 앱 (가정):
- JVM: Java 17 LTS
- Heap: 4GB
- 응답 SLA: P99 < 500ms
권장 옵션:
java -Xms4g -Xmx4g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-Xlog:gc*:file=/var/log/gc.log:time:filecount=10,filesize=10M \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/heap-dumps/ \
-jar ilic-app.jar
옵션 해석:
-Xms4g -Xmx4g: 초기/최대 힙 4GB (동일하게 설정해 동적 조정 방지)-XX:+UseG1GC: G1 명시 (Java 17 기본이지만 명시 권장)-XX:MaxGCPauseMillis=200: 200ms 목표-Xlog:gc*: GC 로그 (G1 모니터링)-XX:+HeapDumpOnOutOfMemoryError: OOM 시 자동 dumpILIC가 다음 조건이면 ZGC 검토:
1. P99 < 100ms 이상 엄격한 SLA
2. Heap > 16GB
3. Java 17+ 사용
4. 처리량이 최우선 아님
옵션:
-XX:+UseZGC
-XX:+UseLargePages (선택)
❌ "성능이 떨어졌어 → GC 옵션 더 추가"
올바른 접근:
1. GC 로그 분석으로 진짜 문제 식별
2. 어디서 시간 소비되는지 측정
3. 한 번에 하나씩 변경
4. 부하 테스트로 효과 검증
5. 문제없으면 옵션 추가 안 함
1. GC 로그 확인:
tail -100 /var/log/gc.log | grep "Pause"
결과:
GC(123) Pause Full (Allocation Failure) 1234.567ms ← Full GC 발동!
2. 원인 분석:
- Old gen 가득 참?
- 큰 객체 할당 시도?
- 메모리 누수?
3. Heap dump 분석:
jmap -dump:format=b,file=heap.hprof <PID>
# → MAT 또는 jhat로 분석
4. 원인 식별 후 해결
ILIC 운영 정기 점검:
☐ GC 로그 일일 모니터링
☐ 평균/P99/P999 GC STW 시간 추적
☐ Full GC 빈도 (이상적 0회/일)
☐ Old gen 사용률 추세
☐ Metaspace 사용률 추세
☐ 응답 시간 vs GC 시간 상관관계
| Q | 핵심 답변 |
|---|---|
| Serial vs Parallel GC 차이? | 단일 vs 멀티스레드. 둘 다 STW |
| CMS의 한계? | Concurrent Mode Failure, 단편화, Java 14에서 제거 |
| 큰 힙에서 GC가 느린 이유? | 영역 전체 처리. 크기에 비례 |
| G1의 핵심 아이디어? | 리전 분할 + 우선순위 회수 + 시간 예측 |
| Pause Prediction Model? | 과거 데이터로 시간 추정. 한도 안에서 효과 큰 리전 선택 |
| Java 8/9/11/17의 기본 GC? | Parallel/G1/G1/G1 |
| ZGC와 G1 차이? | ZGC는 < 10ms 보장. 더 새로운 알고리즘 |
| MaxGCPauseMillis 의미? | GC 목표 시간. 보장은 아님 |
| Full GC가 발생하는 이유? | Old gen 가득. 큰 객체. Metaspace 부족 |
| G1이 약속 못 지키는 경우? | 거대 리전, Full GC 발동, 매우 큰 root set |
1. 기존 GC의 한계 = 큰 힙 시대의 부적합
2. G1 GC의 3가지 혁신
3. ILIC 실무 가이드
-XX:+UseG1GC -XX:MaxGCPauseMillis=200이번 Unit에서 G1이 왜 등장했는지 봤다면, 다음은 리전 모델의 정밀한 구조.
✅ Phase 1 — 자바 변수 ↔ 메모리 매핑 (1.1 ~ 1.6 완주)
✅ Phase 2 — JVM 메서드 실행 메커니즘 (2.1 ~ 2.4 완주)
✅ Phase 3 — 바이트코드와 상수 풀 (3.1 ~ 3.4 완주, 정점)
🚀 Phase 4 — G1 GC 심화
✅ Unit 4.1 참조 카운팅의 한계
✅ Unit 4.2 G1 GC의 등장 배경 ← 여기
⏭ Unit 4.3 리전 기반 레이아웃
⏭ Unit 4.4 거대 리전
⏭ Unit 4.5 우선순위 기반 회수
⏭ Phase 5 — 컬렉션 내부 구조
⏭ Phase 6 — Reflection & Iterator
⏭ Phase 7 — Buffer