IT·리눅스/Java

[Java] GC의 종류 - Serial / Parallel / G1 / ZGC 비교

jmineekim 2026. 5. 8. 10:10

운영 중인 자바 서비스의 응답시간 그래프를 보다가, 평소엔 잘 나오다 가끔 1~2초씩 툭 튀는 구간을 보신 적 있으실 거예요. 거의 다 GC가 STW(Stop-The-World)로 잠깐 세상을 멈춘 흔적입니다. 그래프가 어떻게 튀는지는 어떤 GC를 쓰느냐에 따라 패턴이 완전히 달라지고요. 저도 옛날에 옵션 한 줄을 -XX:+UseG1GC로 바꾸고 응답시간 99분위가 1초에서 200ms로 떨어지는 걸 보고 GC 옵션의 무서움을 실감했습니다. 지난 글에서 JVM 메모리 구조를 정리했으니, 오늘은 그 Heap을 청소하는 GC 네 종류를 한 자리에서 비교해 볼게요.

개념 / 원리

GC가 하는 일은 결국 "Heap에서 안 쓰는 객체 치우기" 한 가지인데, 어떻게 치우느냐에 따라 네 가지로 갈립니다. 핵심 트레이드오프는 두 개예요.

  • Throughput(처리량): 같은 시간에 얼마나 많은 일을 해내느냐.
  • Pause Time(일시중지 시간): GC가 멈추는 동안 애플리케이션이 얼마나 얼어붙느냐.

이 둘은 보통 반비례합니다. 사무실 청소에 비유해 볼게요.

  • Serial GC: 청소부 한 명이 옵니다. 모두 자리를 비운 사이 혼자 다 치워요. 작은 사무실엔 충분합니다.
  • Parallel GC: 청소부 여러 명이 한꺼번에. 그래도 다들 자리는 비워야 합니다. 처리량은 좋지만 STW.
  • G1 GC (Garbage First): 사무실을 구역(Region)으로 잘게 나눠두고, 가장 더러운 구역부터 우선 청소. 큰 부분은 사람들이 일하는 동안 동시에 진행해서 멈추는 시간이 짧습니다.
  • ZGC: 사람들이 일하는 동안 거의 그대로 청소합니다. 일시중지 1ms 미만이 목표. 다만 청소부가 더 많이 필요해요.

표로 정리하면 이렇습니다.

GC 동작 방식 일시중지 적합 힙 크기 활성화 옵션
Serial 단일 스레드 STW 길다 ~수백MB -XX:+UseSerialGC
Parallel 멀티 스레드 STW 중간 수GB -XX:+UseParallelGC
G1 Region 기반, 일부 동시 짧다(목표 200ms) 수GB~수십GB -XX:+UseG1GC
ZGC 거의 전구간 동시(Concurrent) 1ms 미만 수GB~수TB -XX:+UseZGC

기본값도 정리해 두면 헷갈릴 일이 줄어듭니다. Java 8까진 Parallel GC가 기본, Java 9부터는 G1 GC가 기본이고요. ZGC는 Java 11에서 실험 기능으로 도입돼서 Java 15부터 정식 기능이 됐습니다.

실전 예제

GC를 명시적으로 지정하고 로그를 남기는 방법부터 보여드릴게요. Java 17 기준 명령어입니다.

# 1) Parallel GC: 처리량 우선 워크로드 (배치, 데이터 가공)
java -XX:+UseParallelGC -Xms1g -Xmx1g \
     -Xlog:gc*:file=gc.log:time,uptime,level,tags MyApp

# 2) G1 GC: 일반 웹 서비스. 일시중지 목표 200ms
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Xmx4g \
     -Xlog:gc*:file=gc.log:time,uptime,level,tags MyApp

# 3) ZGC: 거대 힙, 초저지연 (대용량 캐시, 게임 서버 등)
java -XX:+UseZGC -Xmx16g \
     -Xlog:gc*:file=gc.log:time,uptime,level,tags MyApp

G1 로그 한 줄을 읽는 법.

[0.345s][info][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 240M->48M(512M) 12.345ms
  • 240M->48M: GC 직후 192MB 회수.
  • (512M): 현재 전체 힙 크기.
  • 12.345ms: 이번에 멈춘 시간. 이 숫자의 분포가 곧 응답시간 꼬리(tail latency)입니다.

실시간 모니터링은 jstat이 제일 빠릅니다.

# 1초마다 GC 사용률 출력
jstat -gcutil <PID> 1000

뽑히는 컬럼은 S0 / S1 / E / O / M / CCS / YGC / YGCT / FGC / FGCT / GCT. Eden(E)이 빠르게 차고 Old(O)가 천천히 늘어가는 게 정상이고, FGC(Full GC) 횟수가 분 단위로 증가하면 거의 사고 직전입니다.

자주 하는 실수

  1. 무조건 ZGC가 좋다는 오해. ZGC는 일시중지가 짧은 대신 CPU와 추가 메모리를 더 씁니다(대체로 한 자릿수 % 오버헤드). 처리량이 더 중요한 배치 잡은 Parallel이 오히려 빠릅니다. "최신=최고"라는 공식은 GC에선 안 통해요.
  2. -XX:+UseG1GC만 켜고 끝내기. G1은 -XX:MaxGCPauseMillis(목표 일시중지)를 같이 잡아야 의미가 살아납니다. 너무 작게 잡으면 GC가 자주 발생해 처리량이 깎이고, 너무 크면 일반 G1과 차이가 없어요. 200ms부터 시작해서 응답시간 그래프 보면서 조정하시는 걸 추천드립니다.
  3. GC 로그를 안 남기는 것. 옵션 한 줄 추가로 끝나는데 이게 없으면 사고 분석이 거의 불가능해집니다. 운영에선 -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=10,filesize=10M 정도는 기본으로 켜두세요. 사고가 나야 그제야 못 켜둔 걸 후회합니다.
  4. 힙 크기와 GC 선택 미스매치. 1GB짜리 마이크로서비스에 ZGC를 쓰면 오버헤드만 큽니다. 반대로 32GB짜리 캐시 서버에 Parallel을 쓰면 Full GC 한 번에 수 초씩 멈춰요. 힙 크기를 먼저 정한 뒤 거기 맞는 GC를 고르는 순서가 맞습니다.

마무리

정리하면 세 줄입니다.

  • 처리량 우선은 Parallel, 응답시간 우선은 G1, 거대 힙 + 초저지연은 ZGC. 작은 데스크탑 도구는 Serial.
  • Java 8까지 Parallel, 9 이상은 G1이 기본. 의도가 분명하지 않다면 기본값에서 출발하시는 게 안전합니다.
  • GC 로그(-Xlog:gc*)와 jstat -gcutil은 무조건 켜두기. 사고 났을 때 이게 있냐 없냐가 분석 시간 30분 vs 3시간을 가릅니다.

다음 글에서는 G1 GC가 Heap을 어떻게 Region으로 쪼개서 "Garbage First"라는 이름값을 하는지, 내부 동작을 한 단계 더 깊이 들여다볼게요.

반응형