G1 의 출시 배경
기존 Parallel / CMS 한계로 인해 만들어짐
핵심은 긴 STW 시간
CMS의 메모리 단편화, 이로인한 pause 시간 예측 불가
한마디로 (긴 STW, 단편화)를 해결하기 위해 출시된 GC이다.
목표는 Pause time을 예측 가능하게 하는 것이다.
G1 GC의 핵심 아이디어
기존 Young/Old 세대 구조를 고정하지 않음
Heap 전체를 균일한 Region으로 분리
크기 : 1~32MB정도 finxed-size region
Young / Survivor / Old / Humongous 등을 region 단위로 동적으로 구성
Garbage-First(G1) 전략
Garbage가 가장 많은 Region부터 회수
Concurrent marking(병렬 마킹) + Copying compaction
Old 영역도 CMS 처럼 concurrent mark 수행
CMS와 달리 copying 방식으로 compact → 단편화 해결
G1 주요 컴포넌트
Remembered Set (RSet)
Region 별로 외부에서 자신을 참조하는 객체 정보 유지
Young/Old 간 참조 때문에 전체 스캔을 하지 않기 위한 핵심 구조
Write Barrier 사용
SATB (Sanpshot-At-The-Beginning)
concurrent marking 시점 기준으로 그 당시 살아 있던 객체를 기준으로 마킹
CMS보다 더 안정적으로 동시 수행 가능
중간 시점에 객체가 죽어도(참조가 끊겨도) 살아있다고 가정하고, 이번 GC에서는 봐주고
다음 GC 사이클에서 처리
이때 참조가 끊긴 (죽은) 객체는 Writer Barrier가 개입하여 기록
비록 중간 과정에서 죽었지만 스냅샷으로 인해 살아난 객체가 있지만
GC 중간에 죽은 객체가 있나 또 확인하는 과정 → STW 발생
때문에 약간의 손해를 보고 속도(Latency)를 얻는다.
G1 GC 사이클
1. Young(Minor) GC (STW)
- Eden -> Survivor로 copy
- Survivor -> Survivor / Old copy
- Region 단위로 빠르게 처리
- STW 발생, 단, 멀티 스레드(Parallel)로 동작하여 짧게 유지하는 것이 목표
2. Concurrent Marking (Old가 찼을 시)
Minor GC가 계속 발생하는 동안 Old 영역을 백그라운드로 분석 (동시)
- Initial Mark (상태 : STW, Minor GC에 piggyback)
- Old 영역의 객체 중에서, GC Root(스택, 스태틱 변수 등)에서 직접 참조하는 객체들을 마킹
- Minor GC를 하려면 어차피 모든 GC Root를 멈추고 스캔, 이때 Old 영역을 참조하는 Root도 같이 찾으면 굳이 마킹을 위해 별도로 STW할 필요가 없음
- 따라서 Minor GC진행할때 Initial Mark를 Piggyback(업혀가기)한다.
- 해당 시점에 SATB 시작점으로 스냅샷을 뜬다. 지금 살아 있는 애들은 마킹 끝날때까지 살아있다고 가정
- Root Region Sacn (상태 : Concurrent)
- Survivor 영역( Root Region)이 참조하고 있는 Old 영역의 객체를 찾아서 마킹
- 다음 Young(Minor) GC 발생 전 무조건 끝내야함
→ Minor GC 발생시 Survivor 영역의 객체들이 이동 혹은 삭제되기에 빨리 스캔을 마쳐야 정확한 참조 가능
- Concurrent Mark (상태 : Concurrent)
- 가장 긴 시간이 소요되는 메인 단계
- 힙 전체(Old 영역 포함)를 뒤지면서 객체 간의 참조를 따라 살아 있는 객체를 찾음
- SATB 알고리즘 작동 : 마킹 도중 객체 참조가 바뀌거나 지워질 수 있으므로 G1은 Write Barrier를 통해 참조 정보를 별도 버퍼에 기록
- 중단 기능 : 이 과정 중 Young(Minor) GC 발생시 마킹 작업 잠시 중단 후 GC가 끝나면 다시 재개
- Remark (상태 : STW - 짧음)
- Concurrent Mark를 최종 마무리
- SATB 버퍼 처리 : Concurrent Mark 동안 애플리케이션이 변경한 참조를 모아둔 로그를 조사하여 최종적으로 살아있는지 확인
- Reference Processing : Weak, Soft, Phantom Reference등을 처리
- Class Unloading : 사용하지 않는 클래스를 메타 스페이스에서 제거
- CMS의 Remark보다 훨씬 빠름 (SATB 덕분에 전체 스캔 필요 X)
- Cleanup (상태 : STW, 일부 Concurrent)
- 계산 및 리전 회수 STW, 내부 자료구조 초기화 : concurrent
- Accounting (계산 - STW) : 각 리전 별 살아있는 객체가 몇 %인가를 확정,
이 점수를 바탕으로 Mixed GC에서 청소할 리전의 우선순위 결정 - Immediate Reclamation (즉시 회수 - STW) : 살아있는 객체가 0%인(완전히 빈) 리전은 Mixed GC까지 기다릴 필요 없이 즉시 힙으로 반환
- Resetting (초기화 - Concurrent) : 다음 마킹을 위해 내부 비트맵 정보를 초기화 작업 - 애플리케이션과 동시 진행
- 계산 및 리전 회수 STW, 내부 자료구조 초기화 : concurrent
각 Region의 live 비율 계산 → Garbage가 많은 Region 우선 순위화
Old 영역에 Garbage가 별로 없다면 Young(Minor) GC만 수행
아니라면 Mixed GC 실행
3. Mixed GC
- Concurrent Marking 후 진행되는 핵심 단계
- Young + Old 일부를 함께 수거
- Old 영역을 한 번에 처리 X
- GC pause 목표 시간 내에서 몇 개의 Region만 선택해 처리
→ Pause Time 예측 가능
좀 더 자세히
정해진 시간(pause time) 안에 쓰레기가 가장 많은 Old Region만 골라서 Young 영역이랑 같이 처리하는 것이 목표
Mixed GC는 한 번의 이벤트가 아닌 여러 번의 GC Pause에 걸쳐서 수행되는 기간
동작 방식
1. 후보군 선정 (Candidate Set) - Concurrent
앞서 Concurrent Marking 단계가 끝나면 G1은 모든 Old Region의 우선순위를 가지고 있다.
G1은 회수 효율이 높은 (쓰레기가 많은) 순서대로 Old Region들을 줄 세운다.
주의할 점은 Old Region의 쓰레기 비율이 높은 순서대로 이므로 해당 Reion에 살아있는 객체도 존재할 가능성이 있다.
2. CSet(Collection Set) 구성과 시간 계산 - STW
실제 GC가 터지면(STW), G1은 계산한다. Cset은 청소할 region들의 목록
필수 포함 : 모든 Young Region
시간 예측:
- 목표 시간: 200ms (설정값)
- Young 처리 예상 시간: 150ms (과거 기록 기반 예측)
- 남은 시간: 50ms
Old Region 추가 (채워 넣기):
- 남은 50ms 동안 처리할 수 있는 만큼만, 아까 줄 세워둔 Candidate Set에서 Old Region을 하나씩 꺼내 CSet에 담습니다.
- "1번 Old Region 옮기는 데 10ms 걸릴 듯? 담아."
- "2번 Old Region 15ms? 담아."
- "3번... 어? 이거 담으면 200ms 넘겠는데? 그만!" -> 여기서 CSet 확정.
3. Evacuation (대피 및 압축) - STW
확정된 CSet에 살아있는 객체들을 새로운 Region으로 복사(Copy)
이과정에서 Old 영역의 단편화가 자연스럽게 해결 (복사시 차곡차곡 쌓기때문)
살아있는 객체란걸 어떻게 확인할까?
- GC Root : stack/static 등에서 직접 참조하는 객체 확인
- 이전에 write barrier가 버퍼에 기록한 것들이 RememberSet에 최종적으로 저장됨
- RSet은 다른 Region에서 나를 참조하는 객체의 주소를 확인
- RSet을 확인하여 해당 Reion만 조사해서 살아있는객체를 판별
남은 시간 (Old 할당량) = 목표시간(pause time) - Young(Minor) GC 예상 시간
4. Full GC
Heap이 가득 찼거나 Humongous Fragmentation등 치명적인 상황에서 수행
Java 8: Single-thread + STW → 기존 Parallel FullGC보다 느림
Java 10~ : Multi-thread 로 처리, 여전히 구조적 복잡함으로 Parallel Full GC보다 느림
튜닝의 목표 : Full GC가 절대 안뜨게 만드는 것
튜닝 포인트
1. Pause Time 줄이기
-XX:MaxGCPauseMillis=200
G1이 해당 목표에 맞춰 region 작업량을 조절, 너무 낮으면 처리량 감소
너무 낮을 시 예상되는 문제
Old 영역을 처리하지 못함 → young에서 계속 old로 승격 → Full GC 발생
pause time을 짧게 잡을 시 G1은 Young 영역의 크기를 강제로 줄임 → GC 빈번 -> CPU가 GC만 처리
2. Young Gen 크기 조정
-XX:G1NewSizePercent
-XX:G1MaxNewSizePercent
기본은 Dynamic
3. Humongous Object 최적화
G1의 고질적 이슈
기본 값 : 1MB
Region size의 50% 이상일 시 Humongous로 취급
별도 region 할당 → FullGC 트리거 원인
따라서 Region Size를 키운다.
-XX:G1HeapRegionSize=16m
더 깊게는 책을 통해서 학습해볼 예정이다.
'JAVA' 카테고리의 다른 글
| GC 로그 찍어서 분석해보기 (0) | 2025.11.24 |
|---|---|
| GC (Garbage Collection) (0) | 2025.11.21 |
| 정규표현식 (0) | 2025.03.06 |
| ENUM - 열거형 (0) | 2025.02.10 |
| 배열의 복사 : clone() 메서드 자바 (0) | 2024.09.03 |