기존 GC(특히 CMS/Parallel GC)는 힙이 커질수록 Stop-the-world(STW) 시간이 길어지는 문제가 있었다.
온라인 서비스, 대규모 자바 서버에서는 몇 초짜리 STW만 나와도 바로 장애로 이어지니, “예측 가능한 짧은 pause”가 핵심 요구사항이 됐다.
G1(Garbage-First) GC는 바로 이 문제를 해결하기 위해 설계된 서버용 저지연 GC다.
-XX:MaxGCPauseMillis 로 지정한 목표 정지 시간 내에서 GC를 끝내려고 노력한다.기존 GC는 Young/Old 세대를 큰 연속 구간으로 나누는 방식이 많았다.
반면 G1은 힙 전체를 고정 크기 region(예: 1~32MB 정도)들로 쪼개서 관리한다.
이 구조 덕분에 G1은 “힙 전체를 한 번에” 보는 대신,
“쓰레기가 많은 region만 골라서” 수집할 수 있다.
이름 그대로 G1의 전략은 “Garbage First”, 즉 쓰레기가 많은 영역 먼저 치우기다.
이렇게 하면:
G1은 “할 수 있는 건 최대한 애플리케이션과 동시에(concurrent) 처리하고,
STW로 해야 하는 부분은 작고 짧게, 병렬(parallel)로 수행하는 전략을 쓴다.
Old 영역 수집은 한 번에 다 멈추고 처리하면 STW가 길어지므로, G1은 Concurrent Marking을 사용한다.
이렇게 마킹이 끝나면, “어떤 region에 얼마나 garbage가 많은지” 정확히 알 수 있고,
이 정보를 바탕으로 Mixed GC에서 Old region도 선택적으로 함께 회수한다.
Concurrent Marking이 끝난 후에는, G1이 Young GC 대신 Mixed GC를 수행한다.
-XX:MaxGCPauseMillisG1이 다른 GC와 가장 다르게 느껴지는 부분이 바로 “정지 시간 목표”를 넣어주는 방식이다.
-XX:MaxGCPauseMillis=200 같이 설정하면,물론 어디까지나 “노력한다(try)”지, 하드 보장은 아니다.
힙이 너무 작거나, 메모리 압박이 심할 때는 Full GC가 발생해서 긴 pause가 날 수 있다.
정리하면, G1 GC는 Stop-the-world 시간을 줄이기 위해 다음 특징들을 가지고 있다.
-XX:MaxGCPauseMillis를 기준으로, Young 크기와 수집 region 개수를 동적으로 조정.- JVM: OpenJDK 17
- GC: G1 GC (-XX:+UseG1GC)
- 힙 크기: -Xms4g -Xmx4g
- Pause 목표: -XX:MaxGCPauseMillis=200
- 워크로드: 다량의 단기 객체 + 일부 장수 객체를 생성하는 웹 API 서버
- GC 로그 옵션: -Xlog:gc*:file=gc.log:tags,uptime,time,level
[2.345s][info][gc,start] GC(5) Pause Young (Normal) (G1 Evacuation Pause)
[2.345s][info][gc,heap ] GC(5) Eden regions: 48->0(52)
[2.345s][info][gc,heap ] GC(5) Survivor regions: 4->6(8)
[2.345s][info][gc,heap ] GC(5) Old regions: 20->20
[2.345s][info][gc ] GC(5) Pause Young (Normal) (G1 Evacuation Pause) 1024M->512M(4096M) 8.5ms
[2.345s][info][gc,cpu ] GC(5) User=0.03s Sys=0.00s Real=0.01s
Pause Young (Normal) (G1 Evacuation Pause)Eden regions: 48->0(52)1024M->512M(4096M) 8.5msReal=0.01sMaxGCPauseMillis=200 목표 안에서 넉넉하게 동작한 상황이다.[120.123s][info][gc] GC(57) Pause Full (G1 Evacuation Pause) 3900M->2100M(4096M) 1.2345678 secs
같은 워크로드에서 Parallel GC를 쓸 때는 GC 한 번에 수백 ms~1초 가까이 멈춤이 나왔지만,
-XX:+UseG1GC -XX:MaxGCPauseMillis=200설정 후에는
Young/Mixed GC pause가 대부분 10~40ms 구간으로 들어오는 걸 확인할 수 있었다.
물론 메모리 압박이 심해지면 여전히 Full GC로 1초 이상 멈출 수 있기 때문에,
힙 크기와 객체 생명주기 설계는 여전히 중요하다.
[참고]
https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html
https://www.perfmatrix.com/g1-garbage-collector-g1gc/
https://www.datadoghq.com/blog/understanding-java-gc/
https://www.linkedin.com/pulse/optimizing-jvm-g1-garbage-collector-g1gc-pratik-ugale-fqzsc