2주차 Unit 4.1 — 참조 카운팅의 치명적 한계

Psj·2026년 5월 15일

F-lab

목록 보기
64/240

Unit 4.1 — 참조 카운팅의 치명적 한계

F-LAB JAVA · 2주차 · Phase 4 · G1 GC 심화
🚀 Phase 4 시작 — 운영 환경 메모리 관리의 깊이로


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • 참조 카운팅(Reference Counting) 의 동작 원리는?
  • 참조 카운팅의 치명적 단점이 정확히 무엇인가?
  • 순환 참조(Circular Reference) 가 왜 참조 카운팅을 무너뜨리나?
  • JVM은 어떤 방식을 채택했고, 그 이유는?
  • Reachability AnalysisGC Roots 의 개념은?
  • Python/Swift는 왜 참조 카운팅을 쓰면서도 살아남나?
  • ILIC 코드에서 순환 참조가 발생하는 패턴은?

🎯 핵심 한 문장

참조 카운팅은 가장 직관적이지만, "순환 참조"라는 단 하나의 결함 때문에 JVM이 채택하지 않았다.
대신 JVM은 Reachability Analysis — GC Roots에서 시작해 도달 가능한 객체만 살리는 방식을 쓴다.
이 선택이 G1 GC, ZGC 같은 현대 GC의 모든 진화의 출발점이다.

비유 — 신문 구독 관리

시스템비유
참조 카운팅"이 신문을 구독하는 사람 수" 카운터
Reachability"독자 명단에서 출발해 추천 인맥 추적"

신문 카운팅:

  • 구독자가 0이 되면 발행 중단
  • 단점: A가 B를 추천하고, B가 A를 추천하면, 둘 다 0이 안 됨

명단 추적:

  • 회사 본부에서 시작해 추천 체인을 따라감
  • 본부에서 도달 불가능한 사람들은 모두 정리
  • 순환 추천이라도 본부 연결이 끊기면 함께 정리

→ JVM이 후자를 선택한 이유.


🧭 9개 섹션 로드맵

1. 1주차 GC 복습 + 더 깊이 들어가는 이유
2. 참조 카운팅 — 가장 직관적인 GC
3. 참조 카운팅의 매력 — 즉시 회수
4. 치명적 결함 — 순환 참조 (★ 핵심)
5. 그 외 단점들 — 갱신 비용, 멀티스레드
6. JVM의 대안 — Reachability Analysis
7. ILIC 실무 — 순환 참조 패턴과 회피
8. 흔한 실수 + 디버깅
9. 면접 질문 + 자기 점검

1️⃣ 1주차 GC 복습 + 더 깊이 들어가는 이유

1.1 1주차에서 본 것

박승제씨가 1주차 Phase 5에서 학습한 GC:

1주차 GC 학습:
  - GC가 왜 필요한가 (수동 메모리 관리의 위험)
  - Young/Old Generation 분리
  - Minor GC vs Major GC
  - Mark-and-Sweep 알고리즘
  - GC가 발동되면 STW(Stop-The-World)
  - GC 종류 (Serial, Parallel, CMS, G1, ZGC) 개요

GC의 큰 그림.

1.2 2주차 Phase 4에서 깊이 들어가는 이유

운영에서 GC가 진짜 문제가 되는 순간:

🚨 운영 서버에서 갑자기 응답 지연
   ↓
GC 로그 확인
   ↓
Full GC가 5초씩 발생
   ↓
"왜?" 라는 질문
   ↓
GC 내부 동작 이해 필수

ILIC 같은 실서비스에서 GC는 시스템 안정성의 핵심.
운영 안정성을 책임지려면 1주차 개론으로 부족.

1.3 Phase 4 전체 그림

Phase 4 — G1 GC 심화

Unit 4.1 (이번) — 참조 카운팅의 한계
  ↓ 왜 JVM이 다른 길을 갔나
  
Unit 4.2 — G1 GC의 등장 배경
  ↓ 큰 힙 시대의 요구
  
Unit 4.3 — 리전(Region) 기반 레이아웃
  ↓ G1의 혁신
  
Unit 4.4 — 거대 리전(Humongous Region)
  ↓ 큰 객체 처리
  
Unit 4.5 — 우선순위 기반 회수
  ↓ "Garbage First" 의 의미

이번 Unit은 출발점.
"왜 단순한 참조 카운팅을 안 쓰고 복잡한 길을 갔나?"


2️⃣ 참조 카운팅 — 가장 직관적인 GC

2.1 동작 원리

각 객체에 참조 카운터(reference counter) 를 둠.

객체 생성:    카운터 = 0
참조 추가:    카운터 +1
참조 제거:    카운터 -1
카운터 = 0:  즉시 회수

2.2 코드로 추적

<Shipment a = new Shipment();   // 카운터 = 1
Shipment b = a;                 // 카운터 = 2
b = null;                       // 카운터 = 1
a = null;                       // 카운터 = 0 → 즉시 회수

각 시점의 메모리:

Step 1: new Shipment() 직후
  Heap: Shipment 객체 (RC=1)
        ↑
       a

Step 2: Shipment b = a;
  Heap: Shipment 객체 (RC=2)
        ↑   ↑
       a   b

Step 3: b = null;
  Heap: Shipment 객체 (RC=1)
        ↑
       a       b(null)

Step 4: a = null;
  Heap: Shipment 객체 (RC=0) → 즉시 회수!
                              ↓
                              메모리 해제
       a(null)  b(null)

단순하고 직관적.
→ 카운터를 매번 갱신만 하면 됨.

2.3 어디서 쓰이나

참조 카운팅을 사용하는 언어/플랫폼:

Python (CPython):       주 GC 메커니즘
Swift / Objective-C:    ARC (Automatic Reference Counting)
PHP:                     주 GC
Perl:                    주 GC
Rust:                    Rc<T>, Arc<T> 명시적
C++ shared_ptr:         라이브러리 차원

JVM, V8 (Node.js), .NET 등 메이저 런타임:

  • 참조 카운팅을 메인으로 쓰지 않음
  • 대신 Reachability Analysis (GC 트레이싱)

2.4 자기 점검 질문

"JVM은 왜 이 단순한 방식을 안 쓰는가?"

답: 순환 참조 + 갱신 비용 + 멀티스레드 의 3가지 문제.
이번 Unit에서 차례로 본다.


3️⃣ 참조 카운팅의 매력 — 즉시 회수

3.1 핵심 장점 — Predictability

참조 카운팅:
  객체가 참조 잃은 순간 → 즉시 메모리 해제
  → 예측 가능한 시점
  → STW 없음
  → 지연 짧음

JVM (Reachability):
  객체가 참조 잃어도 → 다음 GC 사이클까지 살아있음
  → 언제 회수될지 알 수 없음
  → GC 사이클 시 STW 발생
  → 지연 발생

3.2 실시간 시스템에서 매력

실시간 게임:
  - 60 FPS 유지 (16ms마다 화면 갱신)
  - GC가 5ms 멈추면 화면 끊김 발생
  - → STW 없는 참조 카운팅 매력적

iOS 앱 (Swift/Objective-C):
  - 사용자 인터랙션 매 순간 반응성 중요
  - → Apple이 ARC 선택한 이유

3.3 메모리 효율

참조 카운팅:
  - 객체 회수 즉시
  - 메모리 피크 낮게 유지
  - 임베디드 시스템에 적합

Reachability:
  - 객체 회수 지연
  - 일시적으로 메모리 피크 높음
  - 큰 메모리 환경 가정

3.4 그런데 왜 JVM은 안 썼나?

위 장점들이 명확함에도 불구하고 JVM이 안 쓴 이유:

한 줄 답: 순환 참조의 메모리 누수가 치명적.

다음 섹션에서 본다.


4️⃣ 치명적 결함 — 순환 참조 (★ 핵심)

4.1 시나리오 설정

class Shipment {
    Cargo cargo;
}

class Cargo {
    Shipment shipment;
}

void method() {
    Shipment s = new Shipment();
    Cargo c = new Cargo();
    
    s.cargo = c;           // s → c
    c.shipment = s;        // c → s (순환!)
}
// 메서드 종료 후

4.2 참조 카운팅의 동작 추적

Step 1: new Shipment()
  s_obj (RC=1)   ← s
  
Step 2: new Cargo()
  s_obj (RC=1)   ← s
  c_obj (RC=1)   ← c

Step 3: s.cargo = c;
  s_obj (RC=1)   ← s
  c_obj (RC=2)   ← c, s.cargo
  
Step 4: c.shipment = s;
  s_obj (RC=2)   ← s, c.shipment
  c_obj (RC=2)   ← c, s.cargo

Step 5: method() 종료. s, c 지역변수 사라짐.
  s_obj (RC=1)   ← c.shipment만 남음
  c_obj (RC=1)   ← s.cargo만 남음
  
→ 둘 다 카운터가 0이 아님!
→ 회수 불가
→ 메모리 누수

4.3 시각화

메서드 안:

  Stack:                      Heap:
  ┌──────┐                   ┌──────────┐
  │ s    │ ──────────────────► s_obj    │
  └──────┘                   │ RC=2     │
                              │ cargo ───┼──┐
  ┌──────┐                   └──────────┘  │
  │ c    │ ────┐                            │
  └──────┘     │              ┌──────────┐  │
               └─────────────► c_obj    │  │
                              │ RC=2     │  │
                              │ shipment │◄─┘
                              └──────────┘

메서드 종료 후:

  Stack: (비어있음)

  Heap:
  ┌──────────┐
  │ s_obj    │
  │ RC=1     │  ← c.shipment만 가리킴
  │ cargo ───┼──┐
  └──────────┘  │
                │
  ┌──────────┐  │
  │ c_obj    │  │
  │ RC=1     │◄─┘
  │ shipment ┼──┐
  └──────────┘  │
       ▲────────┘
       │
       이 둘은 서로만 가리킴
       바깥에서 아무도 가리키지 않지만
       RC=0이 안 되어서 영원히 메모리 차지

메모리 누수. 참조 카운팅의 결정적 약점.

4.4 ILIC에서 발생할 수 있는 패턴

패턴 1: 양방향 연관 관계

@Entity
public class Shipment {
    @OneToMany(mappedBy = "shipment")
    private List<Cargo> cargoes;
}

@Entity
public class Cargo {
    @ManyToOne
    private Shipment shipment;
}

JPA의 양방향 관계 → 사실상 순환 참조.

패턴 2: Observer 패턴

public class ShipmentEventBus {
    private List<ShipmentListener> listeners = new ArrayList<>();
    
    public void register(ShipmentListener l) {
        listeners.add(l);
    }
}

public class ShipmentService implements ShipmentListener {
    private ShipmentEventBus bus;
    
    public ShipmentService(ShipmentEventBus bus) {
        this.bus = bus;
        bus.register(this);   // ← 순환!
    }
}

bus가 service 가리키고, service가 bus 가리킴 → 순환.

패턴 3: 부모-자식 트리

public class CargoNode {
    private CargoNode parent;
    private List<CargoNode> children;
}

자식이 부모를 가리키고, 부모가 자식 리스트를 가짐 → 순환.

→ 실무 코드에서 순환 참조는 매우 흔함.
→ JVM이 참조 카운팅을 안 쓴 이유.

4.5 Python의 해결책 — Cycle Detector

CPython은 참조 카운팅 + 추가 메커니즘:

주 메커니즘: 참조 카운팅 (즉시 회수)
보조 메커니즘: Cycle Detector (정기적으로 순환 검출)

Cycle Detector:

  • 주기적으로 순환 참조 탐색
  • 발견하면 강제 회수
  • 비싸지만 누수 방지

참조 카운팅의 한계를 인정하고 두 번째 메커니즘 추가.

4.6 Swift의 해결책 — 약한 참조

Swift는 개발자가 명시:

class Shipment {
    var cargo: Cargo?
}

class Cargo {
    weak var shipment: Shipment?   // ← 약한 참조
}

weak:

  • 참조 카운터 증가 안 시킴
  • 대상 객체가 회수되면 자동으로 nil

→ 순환 참조를 개발자가 의식하고 처리.
→ 부담 증가.

JVM은 이 부담을 개발자에게 주지 않는 방식 선택 (Reachability).


5️⃣ 그 외 단점들 — 갱신 비용, 멀티스레드

5.1 갱신 비용

참조 카운팅은 모든 참조 할당에서 카운터 갱신.

Shipment a = new Shipment();   // RC++ on new obj
Shipment b = a;                 // RC++
Shipment c = b;                 // RC++
c = null;                       // RC--
b = null;                       // RC--
a = null;                       // RC-- → 0 → 회수

각 할당마다:

  • 옛 객체의 RC--
  • 새 객체의 RC++
  • RC 0 체크
  • 0이면 회수 (재귀적: 회수 객체가 들고 있던 참조도 RC--)

비용 누적:

  • 단순한 할당이지만 N번 카운터 갱신
  • 빠른 작업이지만 횟수가 많아 누적

5.2 멀티스레드 동기화

여러 스레드가 같은 객체 참조 변경 시:

Thread A: obj의 RC를 5 → 6
Thread B: obj의 RC를 5 → 4 (해제)

만약 동시에 시작했다면?
- 둘 다 5를 읽음
- A가 6 씀, B가 4 씀
- 마지막 쓰기가 이김 → RC 부정확

해결: 원자적 연산 (atomic increment/decrement).

  • CPU 명령어 락 필요
  • 성능 손실
  • 여러 스레드 캐시 무효화 (cache line bouncing)

→ 멀티코어 시대의 큰 부담.

5.3 카운터 자체의 메모리 비용

각 객체가 4~8 bytes의 카운터 필드 보유
1억 개 객체 = 400MB ~ 800MB의 카운터 전용 메모리

작은 객체에서 비율이 큼:

Java의 Object Header: 16 bytes
+ 카운터 4 bytes = 20 bytes

→ Object Header 자체도 큰데, 카운터까지 추가하면 객체 1개의 메모리 효율 떨어짐.

5.4 정리 — 참조 카운팅의 4가지 단점

1. ★ 순환 참조 → 메모리 누수 (가장 치명적)
2. 매 참조 할당마다 카운터 갱신 (성능)
3. 멀티스레드 동기화 비용
4. 객체마다 카운터 필드 추가 메모리

이 4가지 단점이 JVM이 다른 길을 선택하게 만들었음.


6️⃣ JVM의 대안 — Reachability Analysis

6.1 핵심 아이디어

참조 카운팅: "이 객체를 가리키는 사람이 몇 명?"
Reachability: "이 객체가 GC Roots에서 도달 가능한가?"

GC Roots = 반드시 살아있어야 하는 출발점들.
거기서 도달 가능한 객체 = live.
도달 불가능한 객체 = garbage.

6.2 GC Roots란

JVM이 GC Roots로 보는 것들:

1. Stack의 지역 변수
   - 활성 스레드의 모든 스택 프레임의 LVA
   - "지금 실행 중인 메서드들이 들고 있는 객체들"

2. Static 변수
   - 모든 클래스의 static 필드
   - "Method Area의 Static Zone"

3. JNI 참조
   - 네이티브 코드(C/C++)가 들고 있는 자바 객체

4. 활성 스레드 객체 자체

5. 동기화 모니터를 가진 객체

6. JVM 시스템 클래스 (Class loader, etc.)

6.3 알고리즘 — Mark-and-Sweep

1. Mark Phase (마킹):
   - 모든 GC Roots부터 시작
   - 도달 가능한 객체에 "표시" (mark)
   - 그래프 탐색 (BFS/DFS)

2. Sweep Phase (청소):
   - Heap을 순회
   - 마킹 안 된 객체 = garbage
   - 메모리 해제

6.4 순환 참조 문제 해결

GC Roots 시작점들
   ↓
도달 가능한 객체들 마킹
   ↓
A ⇄ B 가 있다면:
   - GC Roots에서 A나 B로 가는 경로 있나?
   - 있으면 → 둘 다 마킹 → 살아있음
   - 없으면 → 둘 다 마킹 안 됨 → garbage

순환이든 아니든 상관없음. 도달 가능성만 본다.

시각화:

  GC Roots
     │
     ▼
  ┌──────┐
  │  A   │ ⇄ ┌──────┐
  └──────┘   │  B   │
             └──────┘

  → GC Roots → A → B 도달 가능 → 둘 다 살아있음
  → 만약 GC Roots → A 연결 끊기면 → A, B 둘 다 도달 불가 → 둘 다 garbage

6.5 비용 vs 이점

항목참조 카운팅Reachability (JVM)
순환 참조❌ 누수✓ 해결
회수 시점즉시GC 사이클까지 지연
갱신 비용매 참조마다0 (할당 시)
멀티스레드어려움차라리 쉬움 (GC 시점만)
STW없음있음
알고리즘 복잡도단순복잡

→ JVM은 순환 참조 해결 + 갱신 비용 0 의 이점을 선택.
→ 단점: STW 지연. 이걸 해결하는 게 G1, ZGC 등의 진화.

6.6 자기 점검 답변

JVM이 참조 카운팅 대신 어떤 방식을 쓰는가?

: Reachability Analysis (또는 Tracing GC).

  • GC Roots에서 시작해 그래프 탐색
  • 도달 가능한 객체만 살리고, 나머지는 garbage
  • 순환 참조 자동 해결
  • 단점은 STW지만, 알고리즘 진화로 최소화 (G1, ZGC)

7️⃣ ILIC 실무 — 순환 참조 패턴과 회피

7.1 좋은 소식 — 자바에선 걱정 적음

JVM은 Reachability라 순환 참조 자체는 메모리 누수 안 만듦.

Shipment s = new Shipment();
Cargo c = new Cargo();
s.cargo = c;
c.shipment = s;
// 순환, 하지만 GC Roots에서 끊기면 둘 다 회수됨

→ Java/Kotlin 개발자는 순환 참조를 신경 쓸 필요 없음.
→ Swift/Python 개발자와 큰 차이.

7.2 그래도 신경 써야 할 진짜 문제

JVM의 메모리 누수는 다른 패턴:

패턴 1: static 컬렉션에 객체 추가

public class ShipmentCache {
    private static final Map<Long, Shipment> cache = new HashMap<>();
    
    public static void add(Long id, Shipment s) {
        cache.put(id, s);
    }
}

문제:

  • cache 는 static → Method Area의 Static Zone
  • GC Roots임 → 영원히 도달 가능
  • cache에 추가된 모든 Shipment 영원히 메모리

해결:

  • WeakHashMap
  • 크기 한정 LRU 캐시
  • 명시적 삭제 정책

패턴 2: ThreadLocal 누수

public class UserContext {
    private static final ThreadLocal<User> CURRENT = new ThreadLocal<>();
    
    public static void set(User u) { CURRENT.set(u); }
    public static User get() { return CURRENT.get(); }
}

문제:

  • Tomcat worker thread 풀 재사용
  • thread가 살아있는 한 ThreadLocal의 User 살아있음
  • 요청 끝나도 정리 안 하면 누적

해결:

try {
    UserContext.set(user);
    process();
} finally {
    UserContext.CURRENT.remove();   // 반드시
}

패턴 3: Listener 등록 후 미해제

eventBus.register(listener);
// ... listener를 어디다 둔 채로 ...

eventBus가 listener 들고 있음 → listener를 가리키는 다른 객체도 함께 살아있음.

해결:

  • eventBus.unregister(listener) 호출
  • WeakReference 사용
  • try-with-resources

패턴 4: ClassLoader 누수 (Unit 3.3에서 본 패턴)

Tomcat redeploy 시 옛 ClassLoader 누수:

  • ClassLoader가 들고 있는 모든 Class
  • Class가 들고 있는 모든 static 필드
  • → Metaspace 누수

7.3 JPA 양방향 연관 — 진짜 문제

@Entity
public class Shipment {
    @OneToMany(mappedBy = "shipment", fetch = FetchType.EAGER)
    private List<Cargo> cargoes;
}

@Entity
public class Cargo {
    @ManyToOne(fetch = FetchType.EAGER)
    private Shipment shipment;
}

문제는 메모리 누수가 아니라:

  • EAGER 로딩 시 무한 루프
  • toString(), equals() 시 StackOverflowError
  • JSON 직렬화 시 무한 재귀

해결:

  • LAZY 로딩
  • @JsonManagedReference / @JsonBackReference
  • toString/equals에서 양쪽 모두 참조 안 함

7.4 메모리 누수 디버깅 도구

# 1. Heap dump 생성
jmap -dump:format=b,file=heap.hprof <PID>

# 또는 OOM 시 자동
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/

# 2. Eclipse MAT (Memory Analyzer Tool)
# - "Histogram": 클래스별 인스턴스 수
# - "Dominator Tree": 메모리 잡고 있는 객체
# - "GC Roots": 회수 안 되는 이유 추적

# 3. JFR (Java Flight Recorder)
java -XX:StartFlightRecording=filename=app.jfr -jar app.jar

7.5 메모리 누수 진단 체크리스트

운영에서 OOM 발생 시:

1. ☐ Heap dump 확보
2. ☐ MAT로 Histogram 분석
   → 의외로 많은 클래스 찾기
3. ☐ Dominator Tree 확인
   → 메모리 잡고 있는 큰 객체
4. ☐ "Path to GC Roots" 추적
   → 왜 회수 안 되는지
5. ☐ 의심되는 패턴:
   - static 컬렉션 (캐시?)
   - ThreadLocal 미정리
   - Listener 미등록
   - JDBC Connection 미반환
   - ClassLoader 누수 (Metaspace)
6. ☐ 코드 수정
7. ☐ 재배포 후 모니터링

7.6 ILIC 운영 베스트 프랙티스

// 1. static 컬렉션은 Caffeine 등 라이브러리 사용
private static final Cache<Long, Shipment> cache = 
    Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(Duration.ofHours(1))
        .build();

// 2. ThreadLocal은 반드시 try-finally
try {
    UserContext.set(user);
    return service.process();
} finally {
    UserContext.clear();
}

// 3. Listener는 약한 참조 또는 명시적 해제
private final List<WeakReference<ShipmentListener>> listeners = ...;

// 4. JDBC는 try-with-resources
try (Connection conn = dataSource.getConnection()) {
    // ...
}  // ← 자동 close

// 5. JPA 양방향은 LAZY + 명시적 fetch
@OneToMany(fetch = FetchType.LAZY)
private List<Cargo> cargoes;

8️⃣ 흔한 실수 + 디버깅

실수 1 — 참조 카운팅과 GC 혼동

면접관: "GC가 어떻게 동작하나요?"

❌ "참조 카운터가 0이 되면 회수합니다"
✅ "Reachability Analysis. GC Roots에서 도달 가능 여부 판단"

→ Java의 GC는 참조 카운팅 아님. 이걸 헷갈리면 면접에서 즉시 감점.

실수 2 — "순환 참조 → 메모리 누수" 오해

class A { B b; }
class B { A a; }

A a = new A();
B b = new B();
a.b = b;
b.a = a;

질문: "이건 메모리 누수인가?"

❌ "예. 순환이라서 회수 안 됨"
✅ "아니오. JVM은 Reachability라 GC Roots에서 끊기면 회수됨"

실수 3 — JVM GC와 Python GC 동일 가정

면접관: "Python GC와 차이는?"

✅ "Python은 참조 카운팅 + Cycle Detector. JVM은 Reachability"
   "Python은 즉시 회수, JVM은 GC 사이클 시 회수"
   "각각 장단점 다름"

실수 4 — WeakReference 남용

// ❌ 모든 객체를 WeakReference로
private final Map<Long, WeakReference<Shipment>> cache = new HashMap<>();

문제:

  • WeakReference는 다음 GC 사이클에 즉시 회수
  • 캐시 효과 거의 없음 (자주 비어버림)

해결:

  • SoftReference (메모리 부족 시 회수)
  • Caffeine 같은 라이브러리

실수 5 — finalize() 신뢰

public class Shipment {
    @Override
    protected void finalize() {
        // 자원 정리?
    }
}

문제:

  • finalize() 는 호출 시점 불확실
  • 호출 안 될 수도 있음
  • Java 9에서 deprecated

해결:

  • AutoCloseable + try-with-resources
  • Cleaner API (Java 9+)

실수 6 — GC 옵션 마구 설정

# ❌ 운영 GC 옵션을 카피해서 적용
java -XX:+UseParallelGC -XX:+UseG1GC ...   # 충돌

해결:

  • 옵션 충돌 확인
  • 메모리 / 워크로드에 맞는 GC 선택
  • Phase 4 다음 Unit들 학습 후 신중히

디버깅 도구 한 번 더

# 1. GC 로그
-Xlog:gc*:file=gc.log

# 2. JFR (Java Flight Recorder)
-XX:StartFlightRecording=duration=60s,filename=app.jfr

# 3. Heap dump
-XX:+HeapDumpOnOutOfMemoryError

# 4. 라이브 분석
jcmd <PID> GC.run
jcmd <PID> GC.heap_info

9️⃣ 면접 질문 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
참조 카운팅 동작 원리?객체마다 카운터. 0이면 회수
JVM이 참조 카운팅 안 쓰는 이유?순환 참조 누수. 갱신 비용. 멀티스레드
순환 참조 시나리오?A → B, B → A. 둘 다 RC=1로 남음
JVM의 대안?Reachability Analysis. GC Roots에서 도달 가능성
GC Roots란?Stack 지역변수, static 필드, JNI 참조, 활성 스레드 등
Python의 GC?참조 카운팅 + Cycle Detector
Swift의 ARC?참조 카운팅 + weak/unowned 참조
Java에서 순환 참조 문제?메모리 누수 X (Reachability). toString/JSON 무한 루프
Java의 진짜 메모리 누수 패턴?static 컬렉션, ThreadLocal, Listener, ClassLoader
Reachability의 단점?STW. GC 사이클 시 멈춤

9.2 자기 점검 체크리스트

기본 이해

  • 참조 카운팅의 동작을 코드로 추적할 수 있다
  • 순환 참조가 왜 문제인지 다이어그램으로 설명할 수 있다
  • Reachability Analysis의 기본 알고리즘을 안다
  • GC Roots 5가지를 안다
  • 참조 카운팅 vs Reachability 장단점 비교 가능

실전 적용

  • Java의 진짜 메모리 누수 패턴을 안다
  • static 컬렉션의 위험을 인식한다
  • ThreadLocal 정리 패턴을 적용한다
  • JPA 양방향 관계의 함정 (무한 루프) 안다
  • heap dump 분석 도구 사용 가능

면접 대비 — 5분 답변

  • 참조 카운팅의 메커니즘과 한계
  • 순환 참조 시나리오 다이어그램
  • JVM의 Reachability 선택 이유
  • Python/Swift와의 비교
  • Java의 진짜 누수 패턴 (참조 카운팅 문제 아님)

🎯 핵심 요약 — 3줄 정리

1. 참조 카운팅은 직관적이지만 치명적

  • 각 객체에 카운터, 0이면 즉시 회수
  • 즉시성 + STW 없음 = 매력적
  • 그러나 순환 참조에서 누수 → 사용 불가

2. JVM의 선택: Reachability Analysis

  • GC Roots에서 그래프 탐색
  • 도달 가능 = 살아있음, 불가능 = garbage
  • 순환 참조 자동 해결
  • 단점: GC 사이클 시 STW → 다음 Unit부터 개선 방법들

3. Java 메모리 누수는 다른 패턴

  • 순환 참조 X (Reachability가 해결)
  • static 컬렉션, ThreadLocal 미정리, Listener 미해제
  • ClassLoader 누수 (Metaspace)
  • 박승제씨가 운영에서 주의할 진짜 패턴들

📚 다음으로...

Unit 4.2 — G1 GC의 등장 배경

이번 Unit에서 JVM이 Reachability를 선택한 이유를 봤다면, 다음은 그 Reachability를 어떻게 효율적으로 수행할 것인가.

  • 1세대 GC (Serial)의 한계
  • Parallel GC, CMS의 진화
  • 큰 힙 시대 (Multi-core + Multi-GB) 의 요구
  • G1 GC의 등장: 정지시간 예측 모델
  • "M밀리초 안에 끝낼게" 의 약속

2주차 진행 상황

✅ Phase 1 — 자바 변수 ↔ 메모리 매핑 (1.1 ~ 1.6 완주)
✅ Phase 2 — JVM 메서드 실행 메커니즘 (2.1 ~ 2.4 완주)
✅ Phase 3 — 바이트코드와 상수 풀 (3.1 ~ 3.4 완주, 정점)
🚀 Phase 4 — G1 GC 심화
  ✅ Unit 4.1 참조 카운팅의 한계 ← 여기
  ⏭ Unit 4.2 G1 GC의 등장 배경
  ⏭ Unit 4.3 리전 기반 레이아웃
  ⏭ Unit 4.4 거대 리전
  ⏭ Unit 4.5 우선순위 기반 회수
⏭ Phase 5 — 컬렉션 내부 구조
⏭ Phase 6 — Reflection & Iterator
⏭ Phase 7 — Buffer
profile
Software Developer

0개의 댓글