지난 글에서 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.123msEden regions: 24->0(28): Eden 24개를 비웠고, 다음 라운드엔 28개 할당 예정.Old regions: 60->58: Old Region 2개 회수. Mixed GC가 동작한 흔적입니다.Humongous regions: 2->2: 큰 객체 2개는 그대로 살아남음.45.123ms: 이번 일시중지. 목표 200ms 이내라 정상.
자주 하는 실수
- Humongous 객체를 모르는 것. Region의 50% 이상을 차지하는 객체는 Old Region에 바로 할당되고 일반 GC 흐름을 못 탑니다. 8MB Region에서 5MB짜리
byte[]를 자주 만들면 Humongous가 폭증해 Mixed GC로 정리가 안 돼요. 큰 배열을 자주 다루는 코드라면G1HeapRegionSize를 키우거나 객체를 쪼개는 쪽이 안전합니다. MaxGCPauseMillis를 너무 작게 잡기. 50ms같이 공격적으로 잡으면 G1이 한 번에 회수할 Region을 줄여서 Mixed GC가 더 자주 돕니다. 처리량이 깎이는 건 덤이고 결국 Old가 차서 Full GC가 터지기도 해요. 200ms부터 시작해 그래프 보면서 내리시는 게 안전합니다.IHOP를 기본값(45%)에서 안 만지기. Heap이 32GB쯤 되면 45%가 14GB라 동시 마킹이 너무 늦게 시작합니다. 마킹 도는 동안 Old가 더 차서 Full GC로 떨어지는 패턴이 흔해요. 큰 Heap에선 35% 정도부터 검토하시면 됩니다.- 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 이야기로 넘어가 볼게요.
'IT·리눅스 > Java' 카테고리의 다른 글
| @Transactional의 함정 - 프록시와 self-invocation (0) | 2026.06.02 |
|---|---|
| [Java] JVM 메모리 누수 - 원인과 진단 도구 한 번에 정리 (0) | 2026.05.12 |
| [Java] GC의 종류 - Serial / Parallel / G1 / ZGC 비교 (0) | 2026.05.08 |
| [Java] JVM 메모리 구조 - Heap, Stack, Metaspace 한 번에 잡기 (0) | 2026.05.07 |
| Java 메서드 완전 정복: 함수 만들기와 호출하기 (1) | 2025.04.30 |