[Java] G1 GC ? 그게 뭔데

HenryHong·2026년 1월 8일

java

목록 보기
15/15
post-thumbnail

1. 왜 G1 GC가 등장했을까?

기존 GC(특히 CMS/Parallel GC)는 힙이 커질수록 Stop-the-world(STW) 시간이 길어지는 문제가 있었다.
온라인 서비스, 대규모 자바 서버에서는 몇 초짜리 STW만 나와도 바로 장애로 이어지니, “예측 가능한 짧은 pause”가 핵심 요구사항이 됐다.

G1(Garbage-First) GC는 바로 이 문제를 해결하기 위해 설계된 서버용 저지연 GC다.

  • 목표: -XX:MaxGCPauseMillis 로 지정한 목표 정지 시간 내에서 GC를 끝내려고 노력한다.
  • 기본 대상: 수백 MB ~ 수십 GB급 큰 힙 + 짧은 응답 시간 요구하는 애플리케이션.

2. Region 기반 설계: 힙을 잘게 쪼개서 필요한 곳만 수집

기존 GC는 Young/Old 세대를 큰 연속 구간으로 나누는 방식이 많았다.
반면 G1은 힙 전체를 고정 크기 region(예: 1~32MB 정도)들로 쪼개서 관리한다.

  • 힙 전체를 N개의 region으로 분할.
  • 각 region은 Eden / Survivor / Old 역할을 동적으로 가질 수 있다.
  • G1은 각 region마다 “얼마나 쓰레기가 많은지(liveness/garbage 비율)”를 추적한다.

이 구조 덕분에 G1은 “힙 전체를 한 번에” 보는 대신,
“쓰레기가 많은 region만 골라서” 수집할 수 있다.

  • STW 시에도 “이번 GC에서 몇 개 region만 처리하겠다”처럼 작게 쪼갤 수 있어서 pause time을 제어하기가 쉽다.

3. Garbage-First: 쓰레기 많은 region부터 우선 회수

이름 그대로 G1의 전략은 “Garbage First”, 즉 쓰레기가 많은 영역 먼저 치우기다.

  • G1은 각 region의 살아있는 객체 비율 / 회수 효율을 계측해서 우선순위 리스트를 유지한다.
  • GC가 필요해지면:
    • 쓰레기 비율이 높은 region들을 먼저 골라서(colletion set),
    • 거기서 살아있는 객체만 다른 region으로 복사(evacuate)하고,
    • 해당 region을 통째로 비우면서 재사용한다.

이렇게 하면:

  • 한 번의 STW 동안 많은 garbage를 회수하면서도,
  • 처리할 region 개수를 제한해서 pause 시간을 제어할 수 있다.

4. 대부분을 Concurrent/Parallel로: STW 구간 최소화 전략

G1은 “할 수 있는 건 최대한 애플리케이션과 동시에(concurrent) 처리하고,
STW로 해야 하는 부분은 작고 짧게, 병렬(parallel)로 수행하는 전략을 쓴다.

4-1. Young GC (짧은 STW)

  • Eden/Survivor region에 대한 young GC는 STW지만 다중 스레드로 병렬 수행된다.
  • 이때 살아있는 객체는 다른 region으로 복사(evacuation), 죽은 객체는 버려지고 region은 비워진다.
  • Young GC 후에 G1은 통계를 기반으로 다음 Young 세대 크기를 자동 조정해서 pause 목표에 맞추려 한다.

4-2. Concurrent Marking (Old 영역은 대부분 동시)

Old 영역 수집은 한 번에 다 멈추고 처리하면 STW가 길어지므로, G1은 Concurrent Marking을 사용한다.

  • 전체 힙의 객체 그래프를 따라가며 “살아있는 객체”를 마킹하는 과정 대부분을 애플리케이션과 동시에(concurrent) 수행.
  • 중간중간 짧은 STW 단계(Initial Mark, Remark 등)는 있지만, 전체 마킹을 Full STW로 하지 않는다.

이렇게 마킹이 끝나면, “어떤 region에 얼마나 garbage가 많은지” 정확히 알 수 있고,
이 정보를 바탕으로 Mixed GC에서 Old region도 선택적으로 함께 회수한다.

4-3. Mixed GC: Young + Old를 조금씩 섞어서

Concurrent Marking이 끝난 후에는, G1이 Young GC 대신 Mixed GC를 수행한다.

  • Eden/Survivor region + garbage가 많은 Old region 일부를 한 번에 수집.
  • 한 번의 GC에서 Old를 다 처리하지 않고, 여러 번의 Mixed GC에 나눠서 회수한다.
  • 이때도 “이번 pause에서 몇 개의 Old region을 포함할지”를 조절해 pause time 목표를 맞추려 한다.

5. Pause Time Goal 기반 튜닝: -XX:MaxGCPauseMillis

G1이 다른 GC와 가장 다르게 느껴지는 부분이 바로 “정지 시간 목표”를 넣어주는 방식이다.

  • -XX:MaxGCPauseMillis=200 같이 설정하면,
    • G1은 “STW pause를 200ms 안쪽으로 유지하려고 노력”한다.
  • 이를 위해:
    • Young 세대 크기를 자동 조절하고,
    • 한 번의 GC에서 처리할 region 수를 동적으로 조정한다.

물론 어디까지나 “노력한다(try)”지, 하드 보장은 아니다.
힙이 너무 작거나, 메모리 압박이 심할 때는 Full GC가 발생해서 긴 pause가 날 수 있다.


6. G1 GC의 요점만 뽑으면

정리하면, G1 GC는 Stop-the-world 시간을 줄이기 위해 다음 특징들을 가지고 있다.

  • Region 기반 힙 관리
    • 고정 크기 region 단위로 힙을 쪼개서, 수집 대상을 유연하게 선택.
  • Garbage-First 전략
    • 쓰레기가 많은 region부터 우선적으로 회수해서 효율적으로 공간 확보.
  • Mostly Concurrent 동작
    • 마킹 등 전체 힙을 보는 작업은 대부분 애플리케이션과 동시에 수행.
  • Parallel + Incremental STW
    • Young/Mixed GC는 STW지만 병렬 + 여러 번에 나눠서 수행해 각 pause를 짧게 유지.
  • Pause Time Goal 기반 튜닝
    • -XX:MaxGCPauseMillis를 기준으로, Young 크기와 수집 region 개수를 동적으로 조정.

7. 실험 환경

- 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

8. Young GC 로그와 해석

[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)
    → 일반적인 Young GC이며, Eden 영역에서 살아있는 객체를 Survivor/Old로 복사(evacuation) 하는 단계다.
  • Eden regions: 48->0(52)
    → GC 전에는 Eden이 48개 region을 쓰고 있었고, GC 후에는 0개, 전체 가능한 Eden 슬롯은 52개 정도라는 뜻.
  • 1024M->512M(4096M) 8.5ms
    → 힙 사용량이 1GB에서 512MB로 줄었고, 이 Young GC에 걸린 STW 시간은 8.5ms 정도.
  • Real=0.01s
    → 애플리케이션이 실제로 멈춰 있던 시간(real)이 약 10ms로, MaxGCPauseMillis=200 목표 안에서 넉넉하게 동작한 상황이다.

9. Full GC가 발생했을 때

[120.123s][info][gc] GC(57) Pause Full (G1 Evacuation Pause) 3900M->2100M(4096M) 1.2345678 secs
  • 메모리 압박이 심하거나, Humongous 객체/조기 승격 등으로 G1이 여유를 잃으면 Full GC가 발생할 수 있다.
  • 이 경우 STW가 1초 이상 길어지기도 하므로,
    • 힙 사이즈를 키우거나,
    • 객체 생명주기/할당 패턴을 튜닝해서 Full GC 빈도를 줄여야 한다.

10. 한 줄 결론

같은 워크로드에서 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

profile
주니어 백엔드 개발자

0개의 댓글