* 이 게시물은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
(자바 버전 여행 ㉒)
버전 얘기를 하면서 문법만 봤지만, 사실 자바를 올릴 때마다 메모리를 청소하는 방식(GC)도 조용히 바뀌어 왔다. 성능 체감의 상당 부분이 여기서 온다. 마지막 모듈의 첫 편은 이 보이지 않는 진화를 정리한다.
GC가 하는 일, 그리고 딜레마
자바는 malloc/free가 없다. 안 쓰는 객체를 GC가 알아서 회수한다. 편하지만 대가가 있다 — 청소하는 동안 애플리케이션이 잠깐 멈추는 Stop-The-World(STW)다. GC의 역사는 이 멈춤을 얼마나 짧게, 예측 가능하게 만드느냐의 싸움이었다.
주요 GC 한눈에
| GC | 성격 | 멈춤 | 쓰임 |
|---|---|---|---|
| Serial | 단일 스레드 | 김 | 작은 앱·컨테이너 |
| Parallel | 처리량 우선 | 보통 | 배치, Java 8 기본 |
| G1 | 균형(영역 분할) | 짧게 관리 | Java 9+ 기본, 범용 |
| ZGC | 초저지연 | 1ms 미만 목표 | 대용량 힙·지연 민감 |
| Shenandoah | 초저지연(대안) | 매우 짧음 | 레드햇 계열 |
버전별 기본 GC
- Java 8: 기본은 Parallel GC. G1은 있었지만 기본 아님.
- Java 9~: 기본이 G1으로 바뀜. 대부분의 앱이 여기서 돈다.
- Java 15~: ZGC가 정식화되고, 21에서는 세대별(Generational) ZGC로 더 실용적이 됨.
- 즉 8에서 17·21로 올리기만 해도, 별 설정 없이 더 나은 GC를 쓰게 된다.
내 앱은 지금 어떤 GC로 도나
$ java -XX:+PrintFlagsFinal -version | grep -iE 'UseG1GC|UseZGC|UseParallelGC'
# 또는 실행 중 GC 로그 보기
$ java -Xlog:gc -jar app.jar
직접 해보기 — GC 확인하고 바꿔 실행하기
지금 내 자바의 기본 GC를 확인하고, 다른 GC로도 돌려 보자.
java -XX:+PrintFlagsFinal -version출력에서UseG1GC가 true인지 본다.- 간단한 프로그램을
-Xlog:gc로 실행해 GC가 언제 도는지 로그를 관찰한다. -XX:+UseZGC로 바꿔 실행하고 로그 형식이 달라지는지 본다.
# 기본 GC 확인
$ java -XX:+PrintFlagsFinal -version | grep UseG1GC
# GC 로그와 함께 실행
$ java -Xlog:gc -XX:+UseZGC MyApp
# (자바 8이라면 UseParallelGC 가 기본으로 보일 것)
실행해 보면 이렇게 나온다.
bool UseG1GC = true (Java 9+ 기본)
[gc] GC(0) Pause Young ... (-Xlog:gc 로그 예)
여기서 조금씩 바꿔 보자.
-Xlog:gc를-Xlog:gc*로 바꿔 더 자세한 로그를 본다.sdk use java 8...로 8에서 같은 명령을 쳐, 기본이 Parallel인지 확인한다.
한 걸음 더 — 힙 크기를 -Xmx100m처럼 작게 주고 객체를 많이 만들어, STW가 로그에 찍히게 해 보자.
✅ 확인 — G1·ZGC의 지향점(멈춤 최소화)과 자바 8·9·21의 기본 GC 흐름을 설명할 수 있으면 통과다.
주요 GC 비교 — 언제 뭘
GC는 하나가 아니라 여럿이고, 각자 ‘처리량 vs 지연’에서 다른 지점을 택한다. 성격을 알면 선택이 쉽다.
| GC | 특징 | 적합 |
|---|---|---|
| Serial | 단일 스레드, 작고 단순 | 작은 앱·작은 컨테이너 |
| Parallel | 처리량(throughput) 우선 | 배치·처리량 중시 |
| G1 | 균형(9+ 기본) | 대부분의 서버 앱 |
| ZGC | 초저지연(멈춤 극소) | 큰 힙·지연 민감 |
| Shenandoah | 초저지연(레드햇) | 지연 민감 |
버전별 기본값의 흐름
자바 8은 Parallel이 기본이었다. 9부터 G1이 기본이 되며 ‘균형’을 택했고, ZGC·Shenandoah가 성숙해 15~17에서 정식이 됐다. 최신 ZGC는 세대별(Generational ZGC, 21)로 더 효율적이 됐다. 즉 버전을 올리는 것만으로도 대개 더 나은 GC 기본값을 자동으로 얻는다 — 이게 ‘보이지 않는 성능 향상’이다.
GC를 바꾸면 빨라질까
흔한 오해다. GC 교체는 ‘처리량 vs 지연‘의 트레이드오프지 만능 가속이 아니다. 응답이 가끔 툭 끊긴다(긴 Stop-The-World)면 ZGC로 지연을 줄이고, 총 처리량이 중요하면 Parallel, 대부분은 G1이 무난하다. 바꾸기 전에 GC 로그로 실제 멈춤 시간을 재는 게 먼저다.
java -XX:+UseZGC -Xlog:gc MyApp # ZGC로 실행 + GC 로그
# G1: -XX:+UseG1GC / Parallel: -XX:+UseParallelGC
측정부터 — 힙 크기가 범인일 때도
감이 아니라 로그로 접근한다. -Xlog:gc로 각 GC의 빈도·멈춤 시간을 보고, 진짜 문제(긴 pause인지, 잦은 GC인지, 메모리 부족인지)를 확인한 뒤 튜닝한다. 의외로 힙 크기(-Xmx)가 너무 작아 GC가 자주 도는 경우가 많다 — GC 옵션을 만지기 전에 힙부터 점검하자. 버전별 비교와 함께 보면 ‘왜 최신 자바가 체감상 빠른가’가 이해된다.
이제 시리즈를 마무리로 접는다. 다음 편은 지금까지 본 8·11·17·21을 한 장의 비교표로 정리한다. 여행의 지도를 완성하는 편이다.
🔧 현장에서 — -Xmx는 사용량이 아니다
운영에서 -Xms16g -Xmx16g인데 실제 사용 메모리는 약 5GB인 상황을 만난 적이 있다. Heap 설정값만 보고 JVM이 16GB를 계속 쓴다고 오해하기 쉽다. Reserved / Committed / Used Heap, 그리고 RSS · Native Memory · Metaspace를 구분해서 봐야 실제 사용량이 보인다.
교훈 — -Xmx는 사용량이 아니라 최대 Heap 제한이다. JVM 메모리는 Used/Committed/Reserved를 반드시 구분하라.
🔧 현장에서 — JFR의 .jfr.part / 0Byte 파일에 속지 마라
운영 서버에 Java Flight Recorder를 걸었더니 .jfr.part 파일이 생기거나 JVM 재시작 후 0Byte Recording이 남는 일이 있었다. Recording 시작 여부 · Disk Recording 여부 · Repository Path · maxAge · maxSize · JVM 종료 과정 · Dump/Flush를 순서대로 확인했다. .part는 Recording 중에 생길 수 있으니 확장자만 보고 손상이라 단정하지 않았다.
교훈 — JFR 분석은 파일 자체가 아니라 JVM Recording Lifecycle을 이해해야 한다.
자주 묻는 질문 (FAQ)
내 앱은 어떤 GC를 쓰나요?
자바 9부터 기본이 G1입니다(자바 8은 Parallel). java -XX:+PrintFlagsFinal -version | grep UseG1GC로 확인합니다.
GC를 바꾸면 빨라지나요?
상황에 따라 다릅니다. 지연이 중요하면 ZGC, 처리량이 중요하면 Parallel이 유리할 수 있습니다. 대부분은 기본 G1로 충분합니다.

답글 남기기