가비지 컬렉션 개요 — 도달 가능성과 GC의 큰 그림

GC가 객체의 생사를 판정하는 도달 가능성(reachability)과 GC Roots, Stop-The-World의 의미, 처리량과 지연 시간이라는 두 평가축, HotSpot GC 계보의 큰 그림을 정리합니다.

· 8 min read · PALDYN Team

지난 글에서 GC의 무대인 힙의 세대 구조를 그렸습니다. 이번 글은 그 무대 위에서 GC가 실제로 하는 일의 큰 그림입니다. GC는 어떻게 “이 객체는 쓰레기다”라고 판정하는지, 왜 모든 GC는 애플리케이션을 멈추는지, GC를 평가하는 두 축은 무엇인지 — 이후 개별 알고리즘(Serial부터 ZGC까지)을 다룰 때 계속 쓰게 될 공통 개념들을 여기서 정리합니다.

쓰레기의 정의 — 도달 가능성

GC의 첫 번째 질문은 “어떤 객체가 쓰레기인가”입니다. Java의 답은 도달 가능성(reachability)입니다. 살아 있는 출발점들에서 참조를 따라가서 닿을 수 있으면 생존, 닿을 수 없으면 쓰레기입니다.

그 출발점이 GC Roots입니다. 대표적으로:

  • 실행 중인 각 스레드 스택의 지역 변수와 파라미터
  • 로드된 클래스의 static 필드
  • JNI가 들고 있는 네이티브 참조
  • 모니터 락이 걸린 객체 등 JVM 내부 참조

도달 가능성과 GC Roots

중요한 따름정리: 서로 참조하고 있어도 루트에서 끊기면 쓰레기입니다. 참조 카운팅 방식(Python의 기본 메커니즘 등)은 순환 참조를 해결하려면 별도 장치가 필요하지만, Java의 루트 기반 추적(tracing)은 순환 참조를 자연스럽게 회수합니다. “A와 B가 서로를 참조하니 메모리 누수”라는 걱정은 Java에서는 불필요합니다.

class Node { Node next; }

void cycle() {
    Node a = new Node();
    Node b = new Node();
    a.next = b;
    b.next = a;   // 순환 참조
} // 메서드 종료 — 스택의 a, b가 사라지면
  // 둘은 서로를 참조하지만 루트에서 끊겨 → 함께 회수

거꾸로 말하면 Java의 메모리 누수는 “해제를 깜빡한” 것이 아니라 “참조를 놓지 않은” 것입니다. static 컬렉션에 계속 쌓기만 하는 캐시, 해제되지 않는 리스너 등록이 전형적인 패턴입니다. 루트에서 닿는 한 GC는 절대 회수하지 않습니다.

모든 GC의 공통 비용 — Stop-The-World

객체 그래프를 탐색하는 동안 애플리케이션이 참조를 바꿔버리면 판정이 어긋납니다. 그래서 GC는 적어도 일부 구간에서 모든 애플리케이션 스레드를 멈춥니다. 이것이 Stop-The-World(STW)입니다.

Stop-The-World와 두 가지 평가축

STW 자체는 피할 수 없습니다 — ZGC 같은 최신 GC도 아주 짧은 STW는 있습니다. GC의 역사는 사실상 “STW를 어떻게 줄이거나 숨길 것인가”의 역사이고, 그 답이 알고리즘마다 다릅니다.

  • 병렬(parallel): 멈춘 시간 동안 여러 GC 스레드로 빨리 끝낸다
  • 동시(concurrent): 애플리케이션이 도는 동안 GC 작업의 대부분을 수행한다 (대신 write barrier 같은 상시 비용 지불)
  • 부분 수집: 한 번에 힙 전체가 아니라 일부 영역만 처리해 멈춤을 잘게 쪼갠다

GC를 평가하는 축도 여기서 나옵니다.

지표의미중요한 워크로드
처리량 (throughput)전체 시간 중 앱이 실행된 비율배치, 대용량 연산
지연 시간 (latency)한 번의 멈춤이 얼마나 긴가API 서버, 실시간 서비스
메모리 오버헤드GC 메타데이터·여유 공간 비용메모리 제약 환경

세 마리 토끼를 모두 잡는 GC는 없습니다. 처리량을 극대화하면 멈춤이 길어지고(Parallel), 멈춤을 극단적으로 줄이면 상시 오버헤드로 처리량을 양보합니다(ZGC). GC 선택은 워크로드의 우선순위 선택입니다.

Minor·Major·Full — GC 이벤트의 어휘

지난 글의 세대 구조와 연결하면 GC 로그에 등장하는 이벤트 용어가 정리됩니다.

  • Minor GC: Young 세대만 수집. 빈번하지만 짧다
  • Major GC: Old 세대 수집을 가리키는 관용적 표현
  • Full GC: Young + Old(+ Metaspace) 전체 수집. 일반적으로 가장 긴 STW — 모니터링에서 경보 대상 1순위
# GC 로그 켜기 (Java 9+ 통합 로깅)
java -Xlog:gc*:file=gc.log:time,uptime,level,tags MyApp

# 로그 한 줄 예시 (G1)
# [12.345s][info][gc] GC(42) Pause Young (Normal)
#   (G1 Evacuation Pause) 512M->128M(1024M) 8.123ms

512M->128M(1024M) 8.123ms — 수집 전 사용량, 수집 후 사용량, 힙 크기, 멈춤 시간. 이 네 숫자만 읽을 줄 알아도 “힙이 부족한가”, “멈춤이 SLA를 위협하는가”를 판단할 수 있습니다. 로그 분석은 챕터 후반(gc-logs-reading)에서 깊이 다룹니다.

HotSpot GC 계보 — 앞으로의 지도

이 챕터에서 차례로 다룰 컬렉터들의 위치를 미리 잡아 두면 좋습니다.

  • Serial: 단일 스레드, 최소 메모리. 컨테이너의 작은 힙·CLI 도구용
  • Parallel: 멀티스레드 STW 수집. 처리량 최우선 (Java 8 기본)
  • CMS: 최초의 동시 수집기. 단편화 문제로 Java 14에서 제거 — 역사적 맥락으로 학습
  • G1: 힙을 리전으로 나눠 멈춤 시간 목표를 지향 (Java 9+ 기본)
  • ZGC / Shenandoah: 힙 크기와 무관한 밀리초 미만~수 밀리초 멈춤. 동시 압축까지 수행

기본값만 따라가도 Java 9 이후는 G1이고, Java 21+에서 ZGC(세대형)가 유력한 대안으로 자리잡았습니다. 어떤 경우에 기본값을 벗어날지가 이 챕터 전체를 관통하는 질문이 될 것입니다.

정리

  • 쓰레기 판정은 GC Roots 기반 도달 가능성 — 순환 참조도 루트에서 끊기면 회수된다
  • Java의 메모리 누수는 “참조를 놓지 않은 것” — static 컬렉션·리스너가 단골이다
  • 모든 GC에는 STW가 있고, GC 알고리즘의 진화는 STW를 줄이고 숨기는 방향이다
  • 평가축은 처리량·지연 시간·메모리 오버헤드 — 셋을 동시에 만족하는 GC는 없다
  • Minor(Young)/Major(Old)/Full(전체)이라는 이벤트 어휘와 GC 로그의 기본 숫자 읽는 법을 익혀 두자

다음 글부터 GC 알고리즘의 뿌리인 Mark-Sweep-Compact부터 하나씩 해부합니다.


지난 글: JVM 메모리 모델 — GC를 이해하기 위한 지도

다음 글: GC 기본 알고리즘 — Mark-Sweep, Mark-Compact, Copying


읽어주셔서 감사합니다. 😊