본문 바로가기

CS/JVM

[JVM 밑바닥까지 파헤치기] HotSpot GC의 상세 구현 기법

이번 글에서는 HotSpot VM이 GC를 효율적으로 수행하기 위해 사용하는 구현 기법들을 정리합니다. 루트 노드 열거의 최적화부터 동시성 GC의 객체 소실 방지까지 다룹니다.

 


루트 노드 열거와 OopMap

Stop the World

루트 노드 열거 작업은 반드시 일관된 스냅숏 상태에서 진행되어야 합니다. 열거 도중에 참조 관계가 바뀌면 정확한 분석이 불가능하기 때문입니다.

그래서 루트 노드 열거 시점에는 모든 애플리케이션 스레드가 멈춥니다. 이를 Stop the World(STW)라고 합니다. 어떤 GC를 사용하든 루트 노드 열거 단계에서는 STW가 발생합니다.

 

문제: GC Roots를 어떻게 빠르게 찾을까?

도달 가능성 분석을 하려면 먼저 GC Roots를 찾아야 합니다. GC Roots 중 하나가 스택 프레임의 지역 변수인데, 순진하게 스택 전체를 스캔하면서 "이게 참조인가?"를 확인하는 건 너무 느립니다.

 

OopMap이란

JIT 컴파일러가 코드를 컴파일할 때, 어느 위치에 어떤 참조가 있는지 미리 기록해둡니다. 이게 OopMap(Ordinary Object Pointer Map)입니다.

메서드 A의 특정 지점:
  - 스택 오프셋 +8: 참조 (User 객체)
  - 스택 오프셋 +16: 참조 (List 객체)
  - 레지스터 rbx: 참조 (String 객체)

GC가 시작되면 스택 전체를 스캔할 필요 없이, OopMap을 보고 참조 위치만 바로 접근하면 됩니다.

 

왜 모든 위치에 OopMap을 만들지 않는가?

코드가 진행되면서 참조 상태가 계속 바뀝니다.

void example() {
    User a = new User();      // 시점 1: a만 참조
    Order b = new Order();    // 시점 2: a, b 참조
    a = null;                 // 시점 3: b만 참조
    String c = "hello";       // 시점 4: b, c 참조
}

매 바이트코드마다 OopMap을 만들면 공간 낭비가 심하고 JIT 컴파일 시간도 늘어납니다. 그래서 특정 지점(Safe Point)의 상태만 저장합니다.

 


Safe Point

GC가 시작되려면 모든 스레드가 Safe Point에 도달해야 합니다. Safe Point는 OopMap이 준비된 지점입니다.

 

Safe Point로 지정되는 곳

  • 메서드 호출 지점
  • 루프 백엣지 (루프 끝에서 처음으로 돌아가는 지점)
  • 예외 처리 지점

 

스레드를 Safe Point로 모으는 방법

선제적 멈춤 (Preemptive Suspension)

GC가 모든 스레드를 강제로 멈춘 다음, Safe Point에 있지 않은 스레드만 다시 실행시켜서 Safe Point까지 가게 하는 방식입니다. 구현이 복잡하고 오버헤드가 커서 현대 JVM에서는 거의 사용하지 않습니다.

자발적 멈춤 (Voluntary Suspension)

각 스레드가 Safe Point에서 스스로 플래그를 확인하고 멈추는 방식입니다. HotSpot이 이 방식을 사용합니다.

[GC 스레드]
플래그 설정: "GC 시작할게, 멈춰!"
    ↓
[애플리케이션 스레드들]
Safe Point 도달 → 플래그 확인 → 멈춤

JIT 컴파일러가 Safe Point마다 플래그 체크 코드를 삽입해둡니다.

 


Safe Region

스레드가 Safe Point까지 도달을 못하는 경우가 있습니다.

Thread.sleep(10000);  // sleep 중인 스레드는 코드를 실행 안 함
synchronized (lock) { ... }  // lock 대기 중

이런 스레드를 계속 기다릴 순 없습니다.

 

Safe Region이란

참조 관계가 변하지 않는 코드 구간을 Safe Region이라고 합니다. 스레드가 Safe Region에 들어가면 "나 지금 안전해"라고 표시하고, GC는 이 스레드를 기다리지 않고 진행합니다.

[스레드]
Safe Region 진입 → "안전 플래그" 설정
    ↓
  (sleep/blocked 상태)
    ↓
Safe Region 탈출 시도 → GC 끝났는지 확인 → 끝났으면 계속 진행

 

Safe Point vs Safe Region

  Safe Point  Safe Region
대상 실행 중인 스레드 대기 중인 스레드 (sleep, blocked)
방식 특정 지점에서 멈춤 구간 전체가 안전함을 표시

 


쓰기 장벽 (Write Barrier)

쓰기 장벽은 참조 대입이 일어날 때 실행되는 추가 코드입니다.

1. 기억 집합 유지

Old → Young 참조가 생기면 카드 테이블을 dirty로 표시해야 합니다.

// 원래 코드
oldObj.field = youngObj;

// 쓰기 장벽이 추가된 실제 실행
oldObj.field = youngObj;
if (isOld(oldObj) && isYoung(youngObj)) {
    cardTable[cardIndex(oldObj)] = DIRTY;
}

2. 동시성 GC의 객체 소실 방지

GC와 애플리케이션이 동시에 실행될 때, 마킹 도중 참조가 바뀌면 살아있는 객체가 실수로 회수될 수 있습니다. 쓰기 장벽으로 이를 감지하고 처리합니다.

 

거짓 공유 (False Sharing) 문제

CPU 캐시는 캐시 라인 단위(보통 64바이트)로 데이터를 읽고 씁니다. 카드 테이블은 1바이트 단위라서, 64개의 카드가 하나의 캐시 라인에 들어갑니다.

[캐시 라인 64바이트]
┌─────────────────────────────────────────────┐
│ 카드0 │ 카드1 │ 카드2 │ ... │ 카드63 │
└─────────────────────────────────────────────┘
    ↑        ↑
 스레드A   스레드B
 
→ 서로 다른 카드인데 같은 캐시 라인이라 동기화 발생

이건 로우 레벨에서 동시성을 다룰 때 발생하는 문제입니다. HotSpot은 조건부 쓰기로 해결합니다.

// 무조건 쓰기 (거짓 공유 발생)
cardTable[index] = DIRTY;

// 조건부 쓰기 (이미 dirty면 쓰지 않음)
if (cardTable[index] != DIRTY) {
    cardTable[index] = DIRTY;
}

JVM 옵션 -XX:+UseCondCardMark로 활성화할 수 있습니다.

 


동시성 GC와 삼색 표시

삼색 표시 (Tri-color Marking)

마킹 상태를 세 가지 색으로 표현합니다.

색상 의미
흰색 아직 방문 안 함 (GC 끝나면 수거 대상)
회색 방문했지만 자식 탐색 미완료 (탐색 대기열)
검은색 방문 완료, 자식도 모두 처리됨

 

탐색 방식

회색 객체가 없어질 때까지 반복합니다.

1. 회색 객체 하나 꺼냄
2. 그 객체의 자식들을 회색으로
3. 그 객체는 검은색으로
4. 회색 없으면 종료 → 흰색은 수거

핵심은 검은색은 다시 안 본다는 것입니다.

 

객체 소실 문제

GC와 애플리케이션이 동시에 실행되면, 마킹 도중에 참조가 바뀔 수 있습니다.

[GC 상태]
A(회색) ──→ C(흰색)
B(검은색)

 

애플리케이션이 참조를 변경합니다.

B.child = C;    // B → C 추가
A.child = null; // A → C 삭제
[변경 후]
A(회색)          C(흰색)
B(검은색) ──→ C

 

GC 입장:

  • A 탐색 → 자식 없음 → A 검은색 완료
  • B는 이미 검은색이라 다시 안 봄
  • C는 흰색 그대로 → 수거됨

실제: B → C 참조가 있으니 C는 살아야 함

 

객체 소실 조건

두 조건이 동시에 만족되면 발생합니다.

조건  의미
조건 1 검은색 → 흰색 참조 추가 (이미 탐색 끝난 객체가 새로 참조)
조건 2 회색 → 흰색 참조 삭제 (원래 그 흰색을 찾아갈 경로 사라짐)

둘 중 하나만 막으면 객체 소실을 방지할 수 있습니다.

 


객체 소실 방지 기법

증분 업데이트 (Incremental Update) — CMS

조건 1을 깨뜨림: 검은색 → 흰색 참조 추가를 감지

[애플리케이션이 B.child = C 실행]

[쓰기 장벽 동작]
"검은색(B)이 흰색(C)을 참조하네? B를 회색으로 되돌림"

[결과]
B가 회색이니까 다시 탐색 대상 → C 발견 → C 생존

SATB (Snapshot-At-The-Beginning) — G1, ZGC, Shenandoah

조건 2를 깨뜨림: 회색 → 흰색 참조 삭제를 기록

[애플리케이션이 A.child = null 실행]

[쓰기 장벽 동작]
"참조가 삭제되네? 삭제되는 값(C)을 기록해둠"

[결과]
나중에 기록된 C를 확인 → C 생존

SATB는 "GC 시작 시점의 스냅숏 기준으로 살아있던 객체는 살린다"는 개념입니다.

 

비교

  증분 업데이트 SATB
감시 대상 새 참조 추가 참조 삭제
동작 검은색 → 회색으로 되돌림 삭제되는 참조를 기록
사용 CMS G1, ZGC, Shenandoah

 


정리

기법 목적
OopMap GC Roots 빠른 탐색
Safe Point OopMap이 준비된 지점, GC 시작점
Safe Region 대기 중인 스레드 처리
쓰기 장벽 기억 집합 유지, 객체 소실 방지
삼색 표시 동시성 GC의 마킹 상태 관리
증분 업데이트 / SATB 객체 소실 방지

이 기법들이 조합되어 HotSpot의 다양한 GC 알고리즘(CMS, G1, ZGC 등)이 구현됩니다.