[자바] 22. GC : 가비지 컬렉터의 진화와 버전별 기본값 (Java 8~21)

자바 버전 여행 22편 - 가비지 컬렉터의 진화

작성자

카테고리:

* 이 게시물은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

(자바 버전 여행 ㉒)

버전 얘기를 하면서 문법만 봤지만, 사실 자바를 올릴 때마다 메모리를 청소하는 방식(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로도 돌려 보자.

  1. java -XX:+PrintFlagsFinal -version 출력에서 UseG1GC가 true인지 본다.
  2. 간단한 프로그램을 -Xlog:gc로 실행해 GC가 언제 도는지 로그를 관찰한다.
  3. -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로 충분합니다.

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다