자바 언어·플랫폼 용어 사전
GC·JIT도달성 · 가비지 컬렉션 · Stop-The-World · 세대 구분

GC

루트에서 도달할 수 없는 객체를 회수하는 것. 참조 개수가 아니라 도달성으로 판단한다.

더 이상 쓸 수 없는 객체의 메모리를 자동으로 회수하는 것.

무엇을 쓰레기로 보나 — 도달성

GC 루트에서 참조를 따라가 '닿을 수 있는가' 로 판단한다

GC 루트

  • 실행 중인 스레드의 스택에 있는 지역 변수
  • static 필드
  • JNI 참조
  • 동기화 모니터로 쓰이는 객체

닿지 않으면 쓰레기 — 참조 개수가 아니다

참조 개수 방식이 아니라서 순환 참조도 회수된다.

A a = new A(); B b = new B();
a.ref = b; b.ref = a;      // 서로 참조
a = null; b = null;        // 루트에서 끊겼다 → 둘 다 회수 대상

왜 세대로 나누나 — 약한 세대 가설

관찰: 대부분의 객체는 만들어지자마자 죽는다

→ 힙을 나눈다
   Young  새 객체. 자주·빠르게 청소한다 (Minor GC)
   Old    살아남은 객체. 가끔 청소한다 (Major/Full GC)

젊은 세대는 살아남는 게 극소수라, '살아남은 것만 복사' 하면 끝난다

  • 쓰레기가 많을수록 오히려 빠르다

할당이 왜 그렇게 빠른가 — TLAB

스레드마다 Eden 의 한 조각을 미리 떼어 준다 (Thread-Local Allocation Buffer)

  • 객체 할당 = 포인터를 옆으로 미는 것뿐 (bump-the-pointer)
  • 동기화가 필요 없다 (내 영역이니까)

그래서 "작은 객체를 많이 만드는 것" 이 생각보다 싸다

Stop-The-World

객체 그래프를 훑는 동안 애플리케이션 스레드를 멈춰야 하는 구간

이 시간을 줄이는 것이 현대 GC 의 목표다

  • 동시 실행(concurrent) · 점진적(incremental) 처리로 STW 구간을 잘게 쪼갠다

어느 GC를 쓰나

  • Serial — 단일 스레드. 아주 작은 힙 · 컨테이너 1코어
  • Parallel — 처리량 우선. 배치 작업에 적합
  • G1 (기본) — 힙을 리전으로 나눠 '쓰레기가 많은 리전부터' 청소
    • 목표 정지 시간을 지정할 수 있다 (-XX:MaxGCPauseMillis)
  • ZGC — 정지 시간이 힙 크기와 무관하게 sub-millisecond
    • 큰 힙 · 낮은 지연이 중요한 서비스. Java 21+ 는 세대별 ZGC
  • Shenandoah — ZGC 와 비슷한 목표 (Red Hat)
-XX:+UseG1GC -XX:MaxGCPauseMillis=200          # 기본
-XX:+UseZGC -XX:+ZGenerational                 # 낮은 지연이 중요할 때
-Xlog:gc*:file=gc.log:time,uptime:filecount=5  # 로그를 남긴다 (필수)

GC 튜닝의 순서

  • ① GC 로그를 남기고 실제 정지 시간·빈도를 본다

  • ② 대부분의 문제는 GC 설정이 아니라 '너무 많이 만드는 코드' 다

    • (불필요한 객체 · 큰 컬렉션 · 문자열 결합 · 박싱)
  • ③ 그다음이 힙 크기

  • ④ 마지막이 GC 알고리즘 교체

  • 순서를 뒤집으면 대개 헛수고한다

면접 함정

  • "참조가 0이면 회수된다" → 참조 카운팅이 아니라 도달성이다.
  • "System.gc()로 청소할 수 있다" → 요청일 뿐 강제가 아니고, 대개 Full GC를 유발해 해롭다.

로그를 읽는 법

[2.456s][info][gc] GC(12) Pause Young (Normal) (G1 Evacuation Pause) 512M->48M(1024M) 12.345ms
                       ^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^
                       종류                     원인                   GC 전->후(전체 힙)      정지 시간

건강한 신호

  • Young GC 가 짧고(수십 ms) 회수량이 크다 (512M → 48M)
  • Full GC 가 거의 없다

나쁜 신호

  • GC 후에도 사용량이 안 줄어든다 (48M → 480M) → 누수
  • Full GC 가 반복된다
  • 정지 시간이 초 단위

누수를 잡는 순서

jcmd <pid> GC.heap_info                     # 현재 상태
jmap -histo:live <pid> | head -20           # 어떤 클래스가 많나 (live 는 Full GC 를 유발한다)
jcmd <pid> GC.heap_dump /tmp/heap.hprof     # 덤프 후 MAT·VisualVM 으로 분석
-XX:+HeapDumpOnOutOfMemoryError             # OOM 시 자동 덤프 (운영에 반드시 켠다)

HeapDumpOnOutOfMemoryError를 미리 켜 두지 않으면 OOM이 났을 때 원인을 알 방법이 사라진다.

컨테이너에서 주의할 것

-XX:MaxRAMPercentage=75.0    # 컨테이너 메모리의 비율로 힙을 잡는다 (-Xmx 고정보다 유연)
# Java 10+ 는 cgroup 을 인식하지만, 힙 밖(메타스페이스·스레드 스택·다이렉트 버퍼)도
# 컨테이너 한도에 포함되므로 100% 를 주면 OOMKilled 된다

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — GC·JIT는 무대 뒤에서 — 내부와 실무 튜닝