IT·리눅스/Java

[Java] G1 GC 내부 동작 - Region과 Garbage First의 정체

jmineekim 2026. 5. 11. 10:00

지난 글에서 G1 GC를 "구역별로 더러운 곳부터 치우는 청소부"에 비유했었는데요, 사실 그 한 줄이 G1의 정체를 거의 다 말해줍니다. 그런데 옵션 한 줄(-XX:+UseG1GC)로 켜고 끝내는 분들을 보면 안타까운 게, Region이 정확히 뭔지, 왜 "가비지 퍼스트"라고 부르는지를 모르면 GC 로그가 외계어로 보이거든요. 저도 처음엔 로그에 Mixed GC라는 단어가 자꾸 떠서 한참을 검색했던 기억이 있습니다. 오늘은 그 G1의 속을 한 칸 더 들여다보면서, 옵션을 어떤 근거로 만지는지까지 정리해 볼게요.

개념 / 원리

G1의 모든 비밀은 Heap을 잘게 쪼갠다는 한 줄로 시작합니다. 기존 GC가 Heap을 Young/Old처럼 큰 덩어리로 보던 것과 달리, G1은 같은 크기의 작은 Region으로 나누고 그 위에 역할을 덮어씌워요.

  • Region 크기는 힙 크기에 따라 1MB~32MB 사이에서 자동 결정 (2의 제곱수 단위).
  • 4GB 힙이면 Region 2MB짜리 약 2,048개.
  • Region마다 한 가지 역할만 갖습니다.
Region 종류 용도 특징
Eden 새 객체가 처음 할당되는 곳 가장 자주 비워짐
Survivor Young GC 한 번 살아남은 객체 Eden ↔ Survivor 이동
Old 여러 번 살아남은 장수 객체 Mixed GC 대상
Humongous Region 크기 50% 이상의 큰 객체 byte[10MB] 같은 것

여기서 핵심은 Eden/Survivor/Old의 위치가 고정되어 있지 않다는 점입니다. 어떤 Region이 어떤 역할을 맡을지는 그때그때 정해지고, 빈 Region은 다음 라운드에 다른 역할로 재활용됩니다.

Garbage First의 진짜 의미

G1이 청소할 때 모든 Region을 다 보지 않습니다. 각 Region의 "쓰레기 비율"을 추적해 두고, 가비지가 가장 많은 Region부터 골라서 회수해요. 이 골라낸 Region 묶음을 CSet(Collection Set) 이라고 부릅니다.

청소부 비유를 이어가면, G1은 매 라운드 시작 전 "오늘은 회의실 중에 제일 더러운 5개만 치우자" 하고 명단을 만든 다음 그 5개만 비우고 들어옵니다. 전체 청소가 아니라 "가성비 높은 곳만"이라 일시중지가 짧은 거고요.

RSet — Region 간 참조 추적

여기 함정이 하나 있습니다. Eden Region 하나만 청소하려 해도 "이 Region 안의 객체를 누가 참조하는지"를 알아야 안전하게 회수할 수 있어요. Heap 전체를 매번 스캔하면 Young GC가 짧을 이유가 없죠.

그래서 각 Region은 RSet(Remembered Set) 이라는 자료구조를 따로 들고 있습니다. RSet은 "내(Region) 안의 객체를 가리키는 외부 Region들의 위치 목록"이에요. Old에서 Young을 새로 참조할 때마다 Write Barrier가 작동해 RSet을 업데이트합니다.

RSet 덕분에 G1은 한 Region을 청소할 때 "이 Region을 참조하는 영역만" 보면 됩니다. Heap 전체 스캔이 사라진 게 G1이 빨라진 핵심 이유입니다.

Young GC vs Mixed GC vs Full GC

  • Young GC: Eden + Survivor만 정리. 빠르고 자주 발생.
  • Mixed GC: Young 영역 + Old Region 일부를 같이 정리. Old가 일정 비율 차면 발동.
  • Full GC: G1이 못 따라가서 어쩔 수 없이 STW로 Heap 전체 정리. 이게 뜨면 튜닝 신호입니다.

실전 예제

지금 JVM이 잡은 Region 크기부터 확인해 봅니다. Java 17 기준입니다.

# 현재 적용된 G1 Region 크기 확인
java -XX:+UseG1GC -Xmx4g -XX:+PrintFlagsFinal -version | grep G1HeapRegionSize
# size_t G1HeapRegionSize = 2097152 {product}
# → 2,097,152 byte = 2MB

Region 크기를 명시적으로 지정하고 GC 로그를 받는 실전 옵션 조합입니다.

java -XX:+UseG1GC \
     -XX:G1HeapRegionSize=8m \
     -XX:InitiatingHeapOccupancyPercent=45 \
     -XX:MaxGCPauseMillis=200 \
     -Xms4g -Xmx4g \
     -Xlog:gc*:file=gc.log:time,uptime,level,tags MyApp

각 옵션이 하는 일.

  • G1HeapRegionSize=8m: Region 크기를 8MB로 고정. Humongous 기준이 4MB가 됩니다.
  • InitiatingHeapOccupancyPercent=45: Old가 전체 Heap의 45%를 넘으면 동시 마킹 시작. 너무 높이면 Mixed GC가 늦게 발동돼 Full GC 위험.
  • MaxGCPauseMillis=200: G1이 CSet 크기를 200ms 안에 끝낼 만큼만 잡도록 유도. 보장이 아니라 목표입니다.

GC 로그에서 Region이 움직이는 줄도 한 번 읽어 봅니다.

[1.234s][info][gc] GC(5) Pause Young (Mixed) (G1 Evacuation Pause)
  Eden regions: 24->0(28) Survivor regions: 4->4(4)
  Old regions: 60->58 Humongous regions: 2->2
  240M->180M(512M) 45.123ms
  • Eden regions: 24->0(28): Eden 24개를 비웠고, 다음 라운드엔 28개 할당 예정.
  • Old regions: 60->58: Old Region 2개 회수. Mixed GC가 동작한 흔적입니다.
  • Humongous regions: 2->2: 큰 객체 2개는 그대로 살아남음.
  • 45.123ms: 이번 일시중지. 목표 200ms 이내라 정상.

자주 하는 실수

  1. Humongous 객체를 모르는 것. Region의 50% 이상을 차지하는 객체는 Old Region에 바로 할당되고 일반 GC 흐름을 못 탑니다. 8MB Region에서 5MB짜리 byte[]를 자주 만들면 Humongous가 폭증해 Mixed GC로 정리가 안 돼요. 큰 배열을 자주 다루는 코드라면 G1HeapRegionSize를 키우거나 객체를 쪼개는 쪽이 안전합니다.
  2. MaxGCPauseMillis를 너무 작게 잡기. 50ms같이 공격적으로 잡으면 G1이 한 번에 회수할 Region을 줄여서 Mixed GC가 더 자주 돕니다. 처리량이 깎이는 건 덤이고 결국 Old가 차서 Full GC가 터지기도 해요. 200ms부터 시작해 그래프 보면서 내리시는 게 안전합니다.
  3. IHOP를 기본값(45%)에서 안 만지기. Heap이 32GB쯤 되면 45%가 14GB라 동시 마킹이 너무 늦게 시작합니다. 마킹 도는 동안 Old가 더 차서 Full GC로 떨어지는 패턴이 흔해요. 큰 Heap에선 35% 정도부터 검토하시면 됩니다.
  4. Full GC 한 번을 가볍게 보는 것. G1에서 Full GC가 뜨면 그건 "G1이 졌다"는 신호예요. 한 번이라도 로그에 Pause Full이 찍히면 IHOP, Region 크기, 힙 크기를 다시 봐야 할 시점입니다. 그냥 두면 트래픽 몰리는 날 더 큰 규모로 같은 일이 일어납니다.

마무리

정리하면 세 줄입니다.

  • G1은 Heap을 같은 크기 Region으로 쪼개고, Eden/Survivor/Old/Humongous 역할을 동적으로 부여합니다.
  • "Garbage First"는 가비지 비율이 높은 Region을 모은 CSet부터 청소한다는 뜻이고, RSet 덕분에 Heap 전체 스캔 없이 그게 가능해집니다.
  • 운영에선 G1HeapRegionSize, IHOP, MaxGCPauseMillis 세 옵션과 GC 로그의 Region 변화 줄만 같이 봐도 거의 다 잡힙니다.

다음 글에서는 ZGC와 Shenandoah가 "거의 멈추지 않는다"를 어떻게 구현하는지, Colored Pointer와 Concurrent Compaction 이야기로 넘어가 볼게요.

반응형