더 이상 쓰이지 않는 객체를 자동으로 찾아 메모리를 회수하는 기능.
"쓰이지 않는다"를 어떻게 판단하나 — 도달 가능성
참조 카운트가 아니라 GC 루트에서 도달할 수 있는가로 판단한다.
GC 루트
├─ 스택의 지역 변수
├─ static 변수
├─ JNI 참조
└─ 실행 중인 스레드
루트 ──► A ──► B ──► C (도달 가능 = 살아 있음)
D ⇄ E (서로 참조하지만 루트에서 도달 불가 = 쓰레기)
참조 카운트 방식이었다면 D⇄E 순환 참조를 회수하지 못한다. 도달 가능성 방식은 이 문제가 없다. 파이썬이 참조 카운트 + 순환 탐지를 따로 쓰는 것과 대비된다.
기본 동작 — Mark and Sweep
- ① Mark — : 루트에서 그래프를 순회하며 살아 있는 객체에 표시 (DFS)
- ② Sweep — : 표시 안 된 객체의 공간을 회수
- ③ Compact: 살아남은 객체를 한쪽으로 모아 단편화 제거 (선택적)
압축(compact)이 필요한 이유 — 회수만 하면 메모리가 조각나 "총 여유는 충분한데 연속 공간이 없어" 할당에 실패할 수 있다.
세대별 수집 — 약한 세대 가설
"대부분의 객체는 만들어지자마자 죽는다" "오래 살아남은 객체는 앞으로도 오래 산다"
이 관찰에 따라 힙을 나눈다.
Young Generation Old Generation
[Eden][Survivor0][Survivor1] ────► [오래 산 객체]
↑ 새 객체 ↑ age 임계 초과 시 승격
Minor GC : Young 만 청소. 자주, 빠르게 (대부분 죽어 있어 회수율이 높다)
Major GC : Old 청소. 드물게, 느리게
Full GC : 전체. 가장 느리다 — 여기가 길어지면 장애
왜 Minor GC가 빠른가 — 살아남은 것만 복사하면 되는데, 대부분 죽어 있으므로 복사할 것이 거의 없다. "죽은 것은 비용이 0" 이다.
수집기 선택 — 실무 판단
| 수집기 | 특징 | 적합 |
|---|---|---|
| Serial | 단일 스레드 | 작은 힙, 단일 CPU |
| Parallel | 여러 스레드로 STW 수행. 처리량 최적 | 배치 작업 |
| G1 (자바 9+ 기본) | 힙을 리전으로 나눠 쓰레기가 많은 곳부터 | 범용. 대부분 이것 |
| ZGC | 대부분을 동시 수행. STW 1ms 미만 | 대용량 힙, 낮은 지연 요구 |
| Shenandoah | ZGC와 유사한 목표 | 저지연 |
G1의 발상 — 이름이 Garbage-First인 이유는 힙을 균등한 리전으로 나눈 뒤
쓰레기 비율이 높은 리전부터 수거하기 때문이다. 적은 작업으로 많이 회수한다.
목표 정지 시간(-XX:MaxGCPauseMillis=200)을 지정하면 그에 맞춰 수거량을 조절한다.
ZGC는 자바 21에서 세대별 모드가 추가되어 실용성이 크게 올랐다. 힙이 수십 GB이고 지연이 중요하면 후보다.
메모리 누수 — GC가 있어도 발생한다
"쓰지 않지만 참조는 남아 있는" 객체는 회수되지 않는다.
| 원인 | 설명 |
|---|---|
| static 컬렉션 | 애플리케이션 생명주기 내내 살아 계속 쌓인다 |
| 캐시 | 크기·TTL 제한이 없으면 무한 증가 |
| 리스너 미해제 | 등록만 하고 해제 안 함 |
| ThreadLocal | 스레드 풀에서 remove() 안 하면 스레드가 살아 있는 한 유지 |
| 커넥션·스트림 | close() 누락 |
ThreadLocal 누수가 특히 골치 아프다. 스레드 풀의 스레드는 재사용되므로 요청이 끝나도 값이 남아, 다음 요청이 이전 사용자의 데이터를 보게 될 수도 있다.
진단
jstat -gcutil <pid> 1000 # 세대별 사용률·GC 횟수를 1초마다
jmap -histo:live <pid> # 클래스별 인스턴스 수 (무엇이 쌓였나)
jcmd <pid> GC.heap_dump a.hprof # 힙 덤프 → MAT/VisualVM 으로 분석
누수의 신호 — Full GC를 해도 Old 사용률이 안 떨어지고 계속 우상향하면 누수다. 톱니 모양으로 오르내리면 정상이다.
튜닝 원칙
- ① 먼저 측정한다 (GC 로그: -Xlog:gc*)
- ② 대부분의 문제는 GC 설정이 아니라 코드 문제(누수·과도한 객체 생성)
- ③ 힙을 무작정 늘리면 GC 시간이 더 길어질 수 있다
- ④ -Xms 와 -Xmx 를 같게 잡아 리사이징을 없앤다
- ⑤ 컨테이너에서는 -XX:MaxRAMPercentage 를 쓴다 (고정값보다 안전)