"대부분의 객체는 금방 죽는다"(약한 세대 가설) 위에 세운 전략.
-
Young 영역 — Eden + Survivor 0 + Survivor 1
- 새 객체는 Eden 에. 가득 차면 Young GC(Minor GC)
- 살아남은 것만 Survivor 로 복사, Eden 은 통째로 비운다
-
Old 영역 — 여러 번 살아남은 객체가 승격(promotion)
- 가득 차면 Old GC — 훨씬 비싸다
왜 복사가 빠른가
죽은 객체는 건드리지 않는다 — 살아 있는 것만 복사한다 생존율이 낮을수록 비용이 낮아진다
덤으로 복사하면서 압축되므로 단편화가 남지 않는다 (할당이 다시 포인터 범프로 돌아간다)
세대 간 참조 문제와 카드 테이블
Young GC 를 하려면 "Young 을 가리키는 모든 참조" 를 알아야 한다 그런데 Old 에 있는 객체가 Young 을 가리킬 수 있다
- Old 전체를 훑으면 Young GC 의 이점이 사라진다
해법: 카드 테이블
-
힙을 512바이트 카드로 나누고, Old 객체가 Young 을 참조하게 되면
-
해당 카드를 'dirty' 로 표시한다 (쓰기 배리어가 이 일을 한다)
-
Young GC 는 dirty 카드만 훑는다
-
참조 대입 한 줄마다 작은 비용이 붙는다는 뜻이기도 하다
승격이 만드는 문제
-
Survivor 가 작으면 아직 죽을 객체가 Old 로 밀려 올라간다
- = 조기 승격(premature promotion)
- Old 가 빨리 차고 Full GC 가 잦아진다
-
증상 — Young GC 후에도 Old 사용량이 계속 우상향
-
대응 — Young 크기 · SurvivorRatio · MaxTenuringThreshold 조정
- 혹은 애초에 오래 사는 객체를 덜 만들도록 설계를 고친다
세대를 안 쓰는 GC 도 있다
ZGC는 초기에 비세대형이었고, JDK 21에서 세대형(Generational ZGC)이 추가돼 JDK 23부터 기본이 됐다. 세대 구분은 공짜가 아니라 트레이드오프(쓰기 배리어 비용 vs 수집 효율)라는 뜻이다.
GC 로그 읽는 법
-Xlog:gc*:file=gc.log:time,uptime,level,tags (Java 9+)
[15.234s][info][gc] GC(42) Pause Young (Normal) (G1 Evacuation Pause)
2048M->512M(4096M) 35.123ms
│ │ │ └ 정지 시간
│ │ └ 전체 힙 크기
│ └ GC 후 사용량
└ GC 전 사용량
봐야 할 것
· GC 후 사용량이 계속 우상향 → 누수 또는 조기 승격
· 정지 시간이 점점 길어짐 → Old 가 차며 Mixed/Full 로 전환 중
· Full GC 가 반복 → 힙 부족. 이 시점엔 이미 서비스가 아프다
왜 '약한' 세대 가설인가
- 약한 세대 가설 — 대부분의 객체는 젊어서 죽는다 → 널리 성립한다
- 강한 세대 가설 — 오래 산 객체일수록 더 오래 산다 → 성립하지 않는 경우가 많다
캐시·세션처럼 '오래 살다가 한꺼번에 죽는' 패턴에서 강한 가설이 깨진다 이런 워크로드에서 세대별 GC 의 이점이 줄어드는 이유다
면접 함정
- ❌ "Old GC = Full GC" → G1의 Mixed GC처럼 Old 일부만 수집하기도 한다.
- ❌ "세대별이 항상 유리" → 대부분이 오래 사는 캐시형 워크로드에서는 이점이 작다.