🎯1주차 Unit 5.1 — GC의 기본 개념과 약한 세대 가설

Psj·2026년 5월 8일

F-lab

목록 보기
37/142

🎯 Unit 5.1 — GC의 기본 개념과 약한 세대 가설 ★★★

F-lab Java 1주차 / Phase 5 / Unit 5.1 본격 학습 자료
9-섹션 마스터 프롬프트 형식으로 깊이 파헤친다.

선수 지식: Unit 4.1 (JVM 런타임 데이터 영역)
다음 Unit: 5.2 — Heap의 세대 구조

이 Unit의 의미: Phase 5 의 시작 — GC 의 출발점.
면접에서 거의 100% 출제 되는 GC 영역의 기본기.
운영 환경에서 OOM, 성능 저하 분석의 토대.


🌍 1. 세상 속 비유

GC = "사무실 청소부"

당신이 큰 사무실에서 일한다고 상상해보세요.

상황:

  • 매일 종이, 컵, 쓰레기가 쌓임
  • 당신은 일에 집중하느라 청소 안 함
  • 사무실은 점점 어수선

해결 — 청소부 (GC):

  • 사무실에 청소부가 있음
  • 사용 안 하는 것 만 자동으로 치움
  • 당신이 청소 신경 쓸 필요 없음

청소부의 일하는 방식:
1. 둘러보기 — "이 컵 누가 쓰나?"
2. 사용 중이면 → 그대로 둠
3. 사용 안 하면 → 버림
4. 정리 후 공간 확보

이게 GC 의 본질.

만약 청소부가 없다면 (C/C++)?

  • 당신이 직접 청소 (free, delete)
  • 깜빡하면 사무실 가득
  • 같은 걸 두 번 버리면 사고

더 직관적인 비유 — "도서관 정리"

도서관에 매일 새 책이 들어옵니다:

  • 자주 빌리는 책 → 계속 보관
  • 안 빌리는 책 → 창고로
  • 오랫동안 안 빌리면 → 폐기

도서관 사서의 정리 전략:
1. 신간 코너 (Eden) — 새 책은 일단 여기
2. 인기 코너 (Survivor) — 가끔 빌리는 책
3. 일반 서가 (Old) — 계속 빌리는 책
4. 폐기 — 아무도 안 빌리는 책

핵심 통찰:

  • "대부분 신간은 금방 폐기됨"
  • "오래 보관한 책은 앞으로도 오래 보관"
  • → 이 패턴을 활용해 정리 효율화

이게 약한 세대 가설(Weak Generational Hypothesis). GC 의 핵심 통찰.


핵심 한 문장

"GC 는 더 이상 참조되지 않는 객체를 자동으로 수거하는 메커니즘이다."

GC의 두 가지 핵심 개념:

  • Garbage: 더 이상 참조되지 않는 객체
  • Collection: 그 Garbage 를 수거하는 행위

비유 정리:

비유 요소GC 적용
청소부GC
사무실Heap
사용 안 하는 컵Garbage (참조 없는 객체)
사용 중인 책상Live 객체 (참조 있음)
청소 시간GC 실행 시간

🔥 2. 탄생 배경

메모리 관리의 두 가지 방식

방식 1: 수동 메모리 관리 (C/C++)

개발자가 직접:

// C 코드
int* arr = malloc(sizeof(int) * 100);  // 할당
// ... 사용 ...
free(arr);  // 직접 해제

장점:

  • 정확한 제어 가능
  • 성능 예측 가능
  • 매우 빠름

단점:

  • 개발자 실수로 메모리 누수
  • 두 번 해제하면 충돌
  • 댕글링 포인터 (이미 해제된 메모리 접근)

방식 2: 자동 메모리 관리 (Java, Python, Go 등)

언어가 자동으로:

// Java 코드
int[] arr = new int[100];  // 할당
// ... 사용 ...
// 해제? 자동으로 GC 가 처리 ✅

장점:

  • 메모리 누수 위험 ↓
  • 보안 ↑
  • 개발 생산성 ↑

단점:

  • GC 실행 중 잠시 멈춤 (STW)
  • 메모리 사용량 ↑
  • GC 튜닝 필요할 수 있음

Java 의 선택 — 자동 GC

자바 (1995) 의 핵심 약속 중 하나:

"메모리 관리는 JVM 이 알아서"

이를 위해 Garbage Collector 도입.

자바 GC 의 영향:

  • C/C++ 의 메모리 버그 90% 해소
  • 개발 속도 ↑
  • 보안 ↑
  • 자바가 엔터프라이즈 시장 장악한 이유 중 하나

GC 의 발전 역사 ⭐

자바 GC 는 계속 진화해왔습니다:

[Java 1.0~1.4]
  - 단순 Mark-Sweep
  - Stop-the-World
  - 매우 느림 (전체 멈춤)

[Java 5]
  - Generational GC 도입 (세대별 GC)
  - Young/Old 분리

[Java 6, 7]
  - Parallel GC (멀티스레드)
  - CMS (Concurrent Mark Sweep)
  - 더 짧은 STW

[Java 9]
  - G1 GC 가 기본
  - 큰 Heap 에 적합

[Java 11+]
  - ZGC (저지연)
  - Shenandoah (저지연)
  - 수십 GB Heap 에서도 ms 단위 STW

[Java 21+]
  - Generational ZGC
  - 더 효율적

GC 진화 = 자바 진화.


GC 가 풀어야 할 핵심 문제 ⭐

기본 질문:

"Heap 의 어떤 객체가 사용 중 이고, 어떤 게 Garbage 인가?"

판단 기준:

  • 사용 중 (Live): 어딘가에서 참조 되고 있음
  • Garbage: 어디서도 참조되지 않음
public void method() {
    String s = "hello";  // s가 String 참조 (Live)
}
// 메서드 종료 → s 사라짐 → "hello" 객체 참조 X → Garbage

참조의 출발점 (GC Root) ⭐ :

  • Stack 의 지역변수
  • Method Area 의 static 변수
  • JNI 참조
  • → 여기서 도달 가능한 객체는 Live

약한 세대 가설 (Weak Generational Hypothesis) ⭐⭐

GC 가 효율적으로 동작하기 위한 가장 중요한 가정:

"대부분의 객체는 금방 죽고, 오래된 객체가 젊은 객체를 참조하는 경우는 드물다."

관찰:
1. 대부분의 객체는 곧 Garbage (지역변수, 임시 객체 등)
2. 일부만 오래 살아남음 (캐시, 싱글톤 등)
3. 오래된 객체 → 젊은 객체 참조는 드뭄

실측 통계:

  • 80~90% 의 객체가 단명
  • 살아남은 객체는 계속 살아남음 경향

의의:

  • GC 가 모든 객체를 같게 다루지 않아도 됨
  • 젊은 객체 영역을 자주 청소
  • 오래된 객체 영역은 가끔만 청소
  • 효율 폭발적 증가

세대별 GC (Generational GC) 의 토대.


핵심 통찰

"GC 는 단순한 청소가 아니라 '약한 세대 가설' 이라는 통찰에서 출발한 정교한 시스템이다."

모든 객체를 같게 다루면 비효율적. 객체의 일생 패턴 (대부분 단명, 일부 장수) 을 활용해 영역을 나누고 다른 전략으로 청소. 이게 자바 GC 의 핵심 통찰.

이 통찰이 없으면 자바 백엔드는 운영 환경에서 너무 자주 멈춤. 통찰이 있어서 수 GB Heap 에서도 ms 단위 STW 가 가능.


💣 3. 없으면 생기는 문제

"GC 모르고 자바 운영하면?"

GC 이해 없이 자바를 운영하면 다양한 문제에 부딪힙니다.


시나리오 1: OutOfMemoryError 직면

// 운영 환경
// 새벽 3시, Slack 알림 폭주
// "API 응답 안 됨"
// 로그 확인:
java.lang.OutOfMemoryError: Java heap space
    at java.util.ArrayList.grow(ArrayList.java:...)
    ...

GC 모르면:

  • "메모리 부족? 늘려!" → -Xmx16g
  • 일시적 해결, 다음 날 또 발생
  • 근본 해결 X

GC 알면:

  • Heap Dump 분석
  • "어떤 객체가 GC 안 되고 있나?"
  • 메모리 누수 코드 찾아냄
  • 근본 해결

시나리오 2: 응답 시간 튐 (Latency Spike)

정상: 평균 응답 시간 50ms
이상: 가끔 500ms ~ 2초

GC 모르면:

  • "DB 가 느린가?"
  • "네트워크 문제?"
  • 원인 못 찾음

GC 알면:

  • GC 로그 확인 (-Xlog:gc*)
  • "Full GC 가 발생했네!"
  • "STW 때문에 응답 지연"
  • → GC 알고리즘 변경 (G1 → ZGC 등)

시나리오 3: ILIC 운임 처리 시 OOM

@Service
public class FareReportService {
    
    public byte[] generateMonthlyReport() {
        List<Fare> allFares = fareRepository.findAll();  // ⚠️
        // 만약 100만 건이면?
        // → Heap 메모리 부족
        // → OOM
    }
}

GC 모르면:

  • "메모리 늘려달라고 인프라팀에 요청"

GC 알면:

  • "전체 Load 는 위험"
  • Streaming 처리, Pagination 적용
  • MemoryConsumptionMonitor 도입

시나리오 4: 메모리 누수 (Memory Leak)

public class FareCache {
    private static final Map<Long, Fare> cache = new HashMap<>();
    
    public Fare get(Long id) {
        return cache.computeIfAbsent(id, this::loadFromDB);
    }
}
// → cache 가 영원히 증가, 절대 GC 안 됨
// → 시간이 지날수록 OOM 가까워짐

GC 모르면:

  • "왜 메모리가 계속 늘지?"

GC 알면:

  • "static 컬렉션은 영원히 살아남음"
  • "참조가 끊기지 않으면 GC 못 함"
  • → WeakHashMap, 캐시 만료 시간 도입

시나리오 5: 면접 탈락

면접 단골:

"GC 가 뭔가요?"
"Stop-the-World 가 뭐죠?"
"약한 세대 가설을 설명해주세요"

답 못함:

  • "음... 메모리 청소 정도?"
  • 시니어 자격 의심

잘 답함:

  • 정확한 정의, 약한 세대 가설, STW 의 의미, 알고리즘 종류
  • → 시니어 후보 인식

시나리오 6: Heap Dump 못 읽음

운영 사고 후:

"Heap Dump 떴습니다. 분석 부탁드려요"

GC 모르면:

  • "어떻게 분석하지...?"
  • → 시니어 답이 아닌 신입 답

GC 알면:

  • MAT (Memory Analyzer Tool) 사용
  • Dominator Tree, Histogram 분석
  • 메모리 누수 패턴 파악
  • → 시니어 답

GC 이해의 중요성 정리

시나리오GC 모르면GC 알면
OOM메모리 늘리기누수 코드 찾기
지연원인 못 찾음GC 튜닝
메모리 누수모니터링만코드 수정
면접탈락시니어 인식
Heap Dump못 읽음정확히 분석

GC 는 시니어 자바 개발자의 필수 영역.


✅ 4. 해결책 — GC 의 기본 개념

Garbage 의 정의 ⭐

Garbage = "더 이상 참조되지 않는 객체"

public void method() {
    Customer c = new Customer("Alice");  // 객체 1: 참조 있음 (Live)
    // ...
    c = new Customer("Bob");  // c 가 객체 2 가리킴
                              // 객체 1 → 참조 없음 → Garbage
}
// 메서드 종료 → c 사라짐 → 객체 2도 Garbage

메모리 그림:

[Heap]
  Customer ("Alice")  ← Garbage (아무도 참조 X)
  Customer ("Bob")    ← 메서드 종료 후 Garbage

[Stack]
  c → 메서드 종료 시 사라짐

Reachability (도달 가능성) — Live 판단 기준 ⭐

GC Root: 참조 추적의 시작점

GC Root 의 종류:
1. Stack 의 지역변수 (모든 활성 스레드)
2. Method Area 의 static 변수
3. JNI 참조 (native 코드)
4. Synchronized lock 객체

Live 판단:

"GC Root 에서 도달 가능한 객체 = Live"
"도달 불가능 = Garbage"

예시:

public class Main {
    static List<Customer> customers = new ArrayList<>();  // GC Root (static)
    
    public static void main(String[] args) {  // args, customers 는 Live
        Customer alice = new Customer("Alice");  // alice 는 Stack → GC Root
        customers.add(alice);  // alice 도 customers 에서 도달 가능
        
        Customer bob = new Customer("Bob");  // bob 은 Stack → GC Root
        // bob 은 customers 에 추가 X
        
        // ...
    }
}

도달 가능성 그래프:

[GC Root]
  - args
  - alice ─────┐
  - bob ───────┤  
  - customers ─┤  (static)
                ↓
[Heap]
  Customer ("Alice")  ← alice 와 customers 양쪽에서 도달 (Live)
  Customer ("Bob")    ← bob 만 도달 (Live)

→ 메서드 종료 시:

  • alice, bob 사라짐
  • customers 는 static (계속 Live)
  • "Alice" 는 customers 에서 도달 가능 → Live
  • "Bob" 은 도달 불가 → Garbage

GC 가 하는 일 ⭐

1. GC Root 식별
        ↓
2. Root 에서 도달 가능한 객체 마킹 (Live)
        ↓
3. 마킹 안 된 객체 = Garbage
        ↓
4. Garbage 메모리 해제
        ↓
5. (선택) 살아있는 객체 압축 (Compact)

5주차 GC 알고리즘 (5.3) 에서 자세히.


Stop-the-World (STW) ⭐⭐

STW = "GC 실행 동안 모든 애플리케이션 스레드 정지"

왜 필요?:

  • GC 가 객체 그래프 분석 중 객체가 변하면 안 됨
  • 일관된 상태에서 마킹/수거

STW 의 영향:

  • 사용자 응답 시간 증가
  • 실시간 시스템에 치명적

예시:

정상 응답: ┃━━━━━━━━━━━━━━━━━━━━┃ (50ms)
STW 발생: ┃━━━━┃[GC: 200ms]┃━━━━━┃ (250ms)

→ STW 를 줄이는 게 GC 진화의 방향.

STW 가 긴 GC:

  • Full GC (Old Generation 전체)
  • Stop-the-World GC (전부)

STW 가 짧은 GC:

  • Minor GC (Young Generation 만)
  • Concurrent GC (CMS, G1, ZGC 등)

약한 세대 가설 (Weak Generational Hypothesis) ⭐⭐⭐

"대부분의 객체는 금방 죽고 (단명),
살아남은 객체는 오래 산다 (장수)."

경험적 통찰:

  • 80~90% 객체가 곧 Garbage
  • 살아남은 10~20% 는 오래 살아남음

일반적 객체 패턴:

public void process() {
    String temp = "임시 데이터";  // 단명 — 메서드 종료 시 Garbage
    List<Integer> list = new ArrayList<>();  // 단명
    // ...
}

@Service
public class FareService {  // 장수 — 애플리케이션 평생
    private FareRepository repository;
}

가설의 활용 ⭐ :

만약 객체를 세대 (Generation) 로 나누면?

  • Young Generation: 새로 만든 객체
  • Old Generation: 오래 살아남은 객체

효과:

  • Young 자주 GC (단명 객체 많아 효율적)
  • Old 가끔 GC (장수 객체 많아 GC 해도 거의 안 줄음)
  • 전체 효율 ↑

이 통찰이 자바 GC 의 핵심.


Generational GC 의 흐름 ⭐ (5.2 미리보기)

1. 새 객체 → Young Generation 의 Eden
        ↓
2. Eden 가득 참 → Minor GC
        ↓
3. 살아남은 객체 → Survivor 영역
        ↓
4. 일정 횟수 살아남으면 → Old Generation Promotion
        ↓
5. Old 가득 참 → Major GC (또는 Full GC)

Unit 5.2 에서 본격.


Reference 의 종류 (간단 미리보기)

GC 에 영향을 미치는 4가지 참조:

1. Strong Reference (일반)

Customer c = new Customer();  // 강한 참조 — 절대 GC X

2. Soft Reference

SoftReference<Customer> sr = new SoftReference<>(new Customer());
// 메모리 부족 시 GC

3. Weak Reference

WeakReference<Customer> wr = new WeakReference<>(new Customer());
// 다음 GC 시 수거

4. Phantom Reference

PhantomReference<Customer> pr = new PhantomReference<>(...);
// GC 발생 후 알림용

Phase 6 에서 자세히.


🏗️ 5. 내부 동작 원리

Mark-Sweep 알고리즘 (가장 기본) ⭐

GC 의 가장 단순한 알고리즘:

단계 1: Mark

[Heap]
  Object1 ───→ Object2
                 ↑
                 │
[Stack]
  ref1 ─────────┘
  
GC: "ref1 에서 시작..."
GC: "Object2 도달 → 마킹"
GC: "Object1 도달 → 마킹"

단계 2: Sweep

[Heap]
  Object1 (마킹 ✓) — 보존
  Object2 (마킹 ✓) — 보존
  Object3 (마킹 ✗) — 제거
  Object4 (마킹 ✗) — 제거

결과:

[Heap]
  Object1
  Object2
  (Object3, 4 자리 비어있음)

단편화 (Fragmentation) 발생.


Mark-Sweep-Compact 알고리즘

Mark-Sweep 후 압축 단계 추가:

Sweep 후:
[Heap]
  [Object1] [____] [Object2] [____] [Object3]
            (빈 공간)

Compact 후:
[Heap]
  [Object1] [Object2] [Object3] [_______________]
                                  (모든 빈 공간이 뒤에)

효과:

  • 단편화 해소
  • 큰 객체 할당 가능

비용:

  • 객체 이동 비용
  • 참조 업데이트 필요

Generational GC ⭐⭐ (자바의 기본)

아이디어: 약한 세대 가설을 활용해 영역 분리.

[Heap]
├── Young Generation (작음, 자주 GC)
│   ├── Eden: 새 객체
│   ├── Survivor 0 (From)
│   └── Survivor 1 (To)
│
└── Old Generation (큼, 가끔 GC)
    └── 장수 객체들

GC 종류:

  • Minor GC: Young 만 → 자주 발생, 빠름
  • Major GC: Old 만 → 가끔 발생, 느림
  • Full GC: 전체 → 매우 가끔, 매우 느림

→ Unit 5.2 에서 자세히.


Card Table — 세대 간 참조 추적 ⭐

문제: Young 객체를 GC 할 때 Old 객체가 참조하면?

[Old Generation]
  oldObj → ...
            ↓ 참조
[Young Generation]
  youngObj

만약 youngObjoldObj 에서만 참조되면 Live 인데, Young 만 스캔하면 못 찾음.

해결 — Card Table ⭐ :

  • Old 영역을 카드 (보통 512 byte) 로 나눔
  • Old → Young 참조가 있는 카드를 dirty 로 마킹
  • Minor GC 시 dirty 카드만 Old 에서 추가 스캔
[Card Table]
  Card 0: clean
  Card 1: dirty ← Young 참조 있음
  Card 2: clean
  Card 3: dirty ← Young 참조 있음

약한 세대 가설 덕분에 dirty 카드 적음 → 효율적.


Stop-the-World 의 메커니즘 ⭐

Safe Point:

  • JVM 이 GC 시작 전 도달하는 안전한 지점
  • 모든 스레드 정지

STW 흐름:

1. GC 트리거 (Heap 가득 등)
        ↓
2. 모든 Application Thread 에 "Safe Point 로!" 신호
        ↓
3. 모든 스레드가 Safe Point 도달
        ↓
4. STW 시작 (모든 Application Thread 정지)
        ↓
5. GC 수행 (Mark, Sweep, Compact)
        ↓
6. STW 종료
        ↓
7. Application Thread 재개

STW 의 길이 = GC 의 핵심 성능 지표:

  • Serial GC: 큼 (전체 단일 스레드)
  • Parallel GC: 줄어듦 (멀티스레드)
  • CMS, G1: 더 짧음 (Concurrent)
  • ZGC: 매우 짧음 (~ms)

Card Table 의 메모리 비용

1GB Heap, 512 byte 카드 → 카드 수 ≈ 200만 개
각 카드 1 byte → Card Table ≈ 2MB

→ Heap 의 0.2% 정도 메모리 비용 — 감내 가능.


GC 트리거 (언제 GC 실행?)

1. 할당 실패

new Object();  // Eden 가득 차서 할당 실패
                // → Minor GC 트리거

2. Old 가득

Old Generation 75% 이상 → Major GC 트리거 (G1 등)

3. 명시적 호출 (권장 X)

System.gc();  // GC 요청 — JVM 이 무시할 수도 있음

⚠️ System.gc() 직접 호출은 비권장 (성능 예측 불가).


GC 가 객체를 처리하는 순서

Marking:
  GC Root → 도달 객체 마킹
  
Reference 처리:
  WeakReference: GC 대상 (다음 사이클)
  SoftReference: 메모리 부족 시
  
Finalization:
  finalize() 메서드 호출 (deprecated)
  
Sweep:
  마킹 안 된 객체 메모리 해제
  
Compact (선택):
  살아있는 객체 압축

finalize() 는 Java 9+ 에서 deprecated. AutoCloseable 사용 권장.


💻 6. 실전 코드 예시

예시 1: GC 동작 관찰

public class GCDemo {
    public static void main(String[] args) {
        System.out.println("시작 메모리: " + getUsedMemory() + " MB");
        
        // 1MB 객체 100개 생성
        List<byte[]> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            list.add(new byte[1024 * 1024]);
        }
        System.out.println("100MB 할당 후: " + getUsedMemory() + " MB");
        
        list = null;  // 참조 끊음
        
        // GC 요청 (강제는 아님)
        System.gc();
        
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        
        System.out.println("GC 후: " + getUsedMemory() + " MB");
    }
    
    private static long getUsedMemory() {
        Runtime runtime = Runtime.getRuntime();
        return (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024);
    }
}

출력 예시:

시작 메모리: 5 MB
100MB 할당 후: 105 MB
GC 후: 5 MB  ← GC 가 100MB 회수

GC 가 정말 동작함을 확인.


예시 2: GC 로그 켜기

# Java 9+
java -Xlog:gc* -jar myapp.jar

# 또는 자세히
java -Xlog:gc*:file=gc.log:time,uptime,level,tags -jar myapp.jar

로그 예시:

[0.123s][info][gc] GC(0) Pause Young (G1 Evacuation Pause) 
                   25M->5M(64M) 12.345ms

해석:

  • 0.123s — JVM 시작 후 시간
  • GC(0) — 0번째 GC
  • Pause Young — Minor GC
  • G1 Evacuation Pause — G1 GC 의 종류
  • 25M->5M(64M) — GC 전 25MB, 후 5MB, 전체 Heap 64MB
  • 12.345ms — 소요 시간

예시 3: 메모리 누수 패턴

// ❌ 메모리 누수 — static 컬렉션
public class FareCache {
    private static final Map<Long, Fare> cache = new HashMap<>();
    
    public static void cache(Fare fare) {
        cache.put(fare.getId(), fare);
        // 한번 들어간 fare 는 절대 GC 안 됨
        // → 시간이 지날수록 메모리 누수
    }
}

왜 누수?:

  • cache 는 static → GC Root
  • 추가된 Fare 는 영원히 참조됨
  • → GC 못 함

해결 1: 만료 시간:

public class FareCache {
    private static final Cache<Long, Fare> cache = 
        Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .maximumSize(1000)
            .build();
}

해결 2: WeakHashMap:

private static final Map<Long, Fare> cache = new WeakHashMap<>();
// 키가 GC 되면 entry 도 자동 제거

예시 4: 메모리 누수 — Listener 등록 후 미해제

// ❌ 누수 가능
public class FareEventBus {
    private final List<FareListener> listeners = new ArrayList<>();
    
    public void register(FareListener listener) {
        listeners.add(listener);
    }
    
    // unregister 메서드 없음 → 등록된 listener 영원히 참조
}

@Component
public class TempFareListener implements FareListener {
    @PostConstruct
    public void init() {
        eventBus.register(this);  // 등록만 하고 해제 안 함
    }
}

해결:

public class FareEventBus {
    private final List<FareListener> listeners = new ArrayList<>();
    
    public void register(FareListener listener) {
        listeners.add(listener);
    }
    
    public void unregister(FareListener listener) {
        listeners.remove(listener);
    }
}

@Component
public class TempFareListener implements FareListener {
    @PreDestroy
    public void cleanup() {
        eventBus.unregister(this);  // ← 해제
    }
}

예시 5: 메모리 누수 — 내부 클래스의 외부 참조

// ❌ 누수 — Inner Class 의 외부 참조
public class FareService {
    private List<byte[]> hugeData = new ArrayList<>();  // 큰 데이터
    
    public Runnable createTask() {
        return new Runnable() {  // Inner Class
            @Override
            public void run() {
                System.out.println("작업 실행");
                // hugeData 사용 안 하지만 외부 참조 자동 보유
            }
        };
    }
}

Runnable task = service.createTask();
service = null;  // FareService 참조 끊음 시도

// 그러나 task 안의 Inner Class 가 FareService 참조 보유
// → service 객체 GC 안 됨
// → hugeData 도 함께 메모리 점유

해결: Static Nested Class 또는 람다:

public Runnable createTask() {
    // 람다 — hugeData 참조 안 하면 외부 참조 X
    return () -> System.out.println("작업 실행");
}

→ Unit 2.6 의 학습이 GC 에 직결.


예시 6: ILIC 의 메모리 효율 패턴

// 큰 데이터 처리 — Stream 활용
@Service
public class FareReportService {
    
    // ❌ OOM 위험
    public byte[] generateReport() {
        List<Fare> all = fareRepository.findAll();  // 100만 건이면?
        return generatePdfFromAll(all);
    }
    
    // ✅ Streaming
    public void streamReport(OutputStream out) {
        try (Stream<Fare> stream = fareRepository.findAllAsStream()) {
            stream.forEach(fare -> appendToPdf(out, fare));
        }
        // 한 번에 메모리에 다 안 올림
    }
}

Spring Data JPA 의 Stream:

@Repository
public interface FareRepository extends JpaRepository<Fare, Long> {
    @Query("SELECT f FROM Fare f")
    Stream<Fare> findAllAsStream();
}

GC 친화적 코드 = 메모리 효율 코드.


예시 7: Heap Dump 생성

# 1. JVM 옵션으로 OOM 시 자동 덤프
java -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/var/log/heap-dump.hprof \
     -jar myapp.jar

# 2. 명시적 덤프
jcmd <pid> GC.heap_dump /tmp/heap-dump.hprof

# 3. jmap (Java 8 이전)
jmap -dump:format=b,file=/tmp/heap-dump.hprof <pid>

분석 도구:

  • MAT (Eclipse Memory Analyzer)
  • VisualVM
  • JProfiler

→ ILIC 운영에서 OOM 사고 시 즉시 덤프 분석.


⚠️ 7. 주의사항 & 흔한 실수

실수 1: System.gc() 호출

public void process() {
    // 처리 ...
    System.gc();  // ❌ 권장 X
}

왜?:

  • JVM 이 무시할 수도 있음
  • 강제 Full GC → 긴 STW
  • 애플리케이션 성능 저하

원칙: GC 는 JVM 에 맡김. 명시적 호출 X.


실수 2: GC 가 모든 메모리 누수 막아준다고 생각

"자바는 GC 가 있으니 메모리 누수 X 다"

진실: GC 는 참조 끊긴 객체 만 수거.

누수 가능 케이스:

  • static 컬렉션
  • ThreadLocal
  • 등록 후 미해제 listener
  • Inner Class 의 외부 참조

GC 가 자동이지만 코드 설계는 책임.


실수 3: finalize() 사용

public class FileHandler {
    @Override
    protected void finalize() throws Throwable {  // ❌ deprecated
        // 정리 작업
    }
}

왜?:

  • 호출 시점 보장 X
  • GC 와 결합되어 성능 문제
  • Java 9+ deprecated, Java 18 제거 예정

해결: AutoCloseable + try-with-resources:

public class FileHandler implements AutoCloseable {
    @Override
    public void close() {
        // 정리 작업
    }
}

try (FileHandler handler = new FileHandler()) {
    // 사용
}  // 자동 close

실수 4: 큰 객체 한 번에 처리

// ❌ OOM 위험
List<Fare> all = repository.findAll();  // 1000만 건?

해결: Pagination 또는 Streaming:

// Pagination
Page<Fare> page = repository.findAll(PageRequest.of(0, 1000));

// Stream
try (Stream<Fare> stream = repository.findAllAsStream()) {
    stream.forEach(this::process);
}

실수 5: Heap 만 늘리면 된다

java -Xmx32g -jar myapp.jar  # ❌ 근본 해결 X

문제:

  • 메모리 누수면 32GB 도 결국 가득
  • Full GC 시 STW 더 길어짐
  • 비용 ↑

해결: 코드 레벨 메모리 분석 + GC 튜닝.


실수 6: GC 알고리즘 무시

java -jar myapp.jar  # 기본 GC 사용

Java 8 의 기본: Parallel GC
Java 9+ 의 기본: G1 GC
상황에 따라: ZGC, Shenandoah 등

원칙:

  • 작은 Heap (~4GB): Parallel 또는 G1
  • 중간 Heap (4~32GB): G1
  • 큰 Heap (32GB+): ZGC, Shenandoah
  • 저지연 우선: ZGC

실수 7: GC 로그 안 봄

운영 환경:

  • GC 로그 미설정
  • → 문제 발생 시 분석 불가

해결: 항상 GC 로그 활성화:

java -Xlog:gc*:file=gc.log:time,uptime,level,tags \
     -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/var/log/ \
     -jar myapp.jar

🔗 8. 연관 개념 맵

Phase 5 (GC) 내 흐름

[Unit 5.1: GC 기본 개념과 약한 세대 가설]  ← 지금 여기
        ↓
[Unit 5.2: Heap 의 세대 구조]
        ↓
[Unit 5.3: GC 알고리즘 종류] (있다면)

Phase 4 와의 통합

학습GC 관점
Unit 4.1 (Heap)GC 의 주 대상
Unit 4.1 (Stack)GC Root 의 출발점
Unit 4.2 (Pass by Value)객체 참조의 동작
Unit 2.6 (Inner Class)외부 참조 보유 → GC 영향

Phase 4 가 GC 이해의 토대.


미래 주차와의 연결

4주차 (동시성):

  • 멀티스레드와 GC 의 상호작용
  • GC 가 Application Thread 정지

5주차 (Spring):

  • Bean 의 Singleton — Old Generation
  • Prototype — Young Generation

11-12주차 (JPA):

  • 영속성 컨텍스트 메모리
  • Detach 의 의미

13-14주차 (DB/Cache):

  • Redis 캐시 vs JVM 캐시
  • WeakHashMap 활용

18주차 (Spring Security):

  • 세션 메모리 관리

자바 vs 다른 언어 GC

언어GC 방식
JavaGenerational, Mark-Sweep-Compact
C#/.NETGenerational, 비슷
GoConcurrent Mark-Sweep
PythonReference Counting + Cycle Detection
JavaScript (V8)Generational
RustGC 없음 (소유권 시스템)

자바 GC 는 가장 정교한 GC 중 하나.


면접 단골 질문 매핑

질문이 Unit에서의 답
"GC 가 뭔가요?"참조 끊긴 객체 자동 수거
"Garbage 의 정의?"어디서도 도달 불가능한 객체
"약한 세대 가설?"대부분 객체는 단명, 살아남은 것은 장수
"STW 가 뭔가요?"GC 동안 모든 스레드 정지
"Mark-Sweep?"마킹 + 수거 알고리즘
"GC Root?"Stack 지역변수, static, JNI 등

📝 9. 핵심 요약 — 3줄 정리

1️⃣ GC 는 더 이상 참조되지 않는 객체를 자동 수거하는 메커니즘이다.

Garbage = 어디서도 도달 불가능한 객체. GC Root (Stack 지역변수, Method Area static, JNI 참조) 에서 출발해 Reachability 분석으로 Live 객체 마킹, 나머지를 수거. C/C++ 의 수동 메모리 관리의 위험 (메모리 누수, 댕글링 포인터) 을 해결한 자바의 핵심 약속.

2️⃣ 약한 세대 가설이 자바 GC 효율의 비밀이다.

"대부분 객체는 단명, 살아남은 것은 장수" — 이 통찰로 Heap 을 Young Generation (자주 GC) 과 Old Generation (가끔 GC) 으로 분리. Young 의 Eden, Survivor 영역에서 객체가 일정 횟수 살아남으면 Old 로 Promotion. Card Table 로 Old → Young 참조를 추적해 Minor GC 효율 유지.

3️⃣ STW 와 GC 알고리즘 이해가 시니어의 차별화 영역이다.

Stop-the-World = GC 동안 모든 Application Thread 정지. STW 의 길이가 응답 시간에 직결. Java 진화: Serial → Parallel → CMS → G1 → ZGC → Shenandoah, STW 를 줄이는 방향. 운영 환경에서 GC 로그 (-Xlog:gc*) + Heap Dump (-XX:+HeapDumpOnOutOfMemoryError) + MAT 분석은 시니어의 필수 도구. System.gc() 호출, finalize() 사용은 안티패턴.


🎓 학습 자기 점검

기본 이해

  • GC 의 정의를 한 문장으로 설명할 수 있다
  • Garbage 의 판단 기준 (Reachability) 을 안다
  • GC Root 의 종류를 나열할 수 있다
  • 약한 세대 가설의 두 명제를 안다

실전 적용

  • ILIC 코드의 메모리 누수 패턴을 식별할 수 있다
  • static 컬렉션 사용 시 위험을 인지한다
  • WeakHashMap, 캐시 만료 등 누수 방지 패턴을 안다
  • GC 로그를 켜고 분석할 수 있다

면접 대비 (3-5분 답변)

  • "GC 가 뭔가요?" 답변 가능
  • "약한 세대 가설?" 답변 가능
  • "STW 와 그 영향?" 답변 가능
  • "메모리 누수 발생 패턴?" 답변 가능

자기 점검 질문 답변

Q1: 약한 세대 가설이 맞다면 메모리 구조를 어떻게 설계하는 게 합리적인가?

한 줄 답: 세대별로 영역을 분리 (Young / Old) 해서 각자 다른 GC 전략 적용.

상세 설계 — Generational Heap:

[Heap]
├── Young Generation (작음)
│   ├── Eden (새 객체)
│   ├── Survivor 0 (1차 생존)
│   └── Survivor 1 (2차 생존)
│
└── Old Generation (큼)
    └── 장수 객체

왜 이렇게?:

가설 1: "대부분 객체는 단명"

  • → Young Generation 을 작게 유지
  • 자주 GC 해도 살아있는 객체 적어 효율적
  • → Mark-Sweep-Compact 가 빠름

가설 2: "살아남은 객체는 장수"

  • → Old Generation 에 계속 살아있는 객체들
  • → Old GC 해도 거의 안 줄음 → 가끔만 GC
  • → 효율 ↑

Young GC 알고리즘 — Copying GC (Eden + Survivor):

1. 새 객체 → Eden 에 할당
2. Eden 가득 → Minor GC
3. Eden + Survivor 0 의 살아있는 객체 → Survivor 1 로 복사
4. Eden, Survivor 0 비움
5. 다음 GC 시 역방향 (Survivor 1 → Survivor 0)
6. N회 살아남으면 → Old 로 Promotion

효율적인 이유:

  • 살아있는 객체 적음 → 복사 비용 낮음
  • 단편화 자동 해소 (Compact 효과)

Old GC 알고리즘 — Mark-Sweep-Compact:

  • 객체 많고 살아있는 비율 높음
  • 복사 비효율 → Sweep + Compact

Card Table 도입:

  • Old → Young 참조 추적
  • Minor GC 시 Old 전체 스캔 회피

→ 이 모든 것이 약한 세대 가설 의 자연스러운 결과.

자바의 실제 = HotSpot JVM 이 정확히 이 구조 채택.


Q2: "GC 튜닝" 이 일반적으로 무엇을 줄이는 것인가?

한 줄 답: STW (Stop-the-World) 시간 을 줄이는 것.

상세 설명:

GC 튜닝의 주요 목표는 3가지 지만, 가장 흔한 것은 STW:

1. STW 시간 최소화 ⭐ (가장 흔함)

왜 중요?:

  • STW 동안 사용자 응답 X
  • API 응답 시간 튐
  • 실시간 시스템에 치명적

측정:

  • 평균 GC 시간
  • 최대 GC 시간 (P99)
  • GC Pause 분포

튜닝 방법:

  • GC 알고리즘 변경: Parallel → G1 → ZGC
  • Heap 크기 조정 (너무 크면 Full GC 길어짐)
  • Young Generation 크기 조정

예시:

# Parallel GC (Java 8 기본): STW 길음
java -XX:+UseParallelGC -Xmx8g

# G1 GC (Java 9+ 기본): STW 짧음
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Xmx8g

# ZGC (Java 11+): STW 매우 짧음 (~ms)
java -XX:+UseZGC -Xmx8g

2. Throughput 최대화

Throughput = 애플리케이션 작업 시간 / 전체 시간

왜 중요?:

  • 배치 작업, 데이터 처리에 중요
  • "총 처리량"이 핵심

튜닝 방법:

  • Parallel GC 사용 (Throughput 우선)
  • Heap 크게 설정 (GC 빈도 ↓)
# Parallel GC + Throughput 우선
java -XX:+UseParallelGC -XX:GCTimeRatio=99 -Xmx16g

3. Footprint (메모리 사용량) 최소화

왜 중요?:

  • 컨테이너 환경 (제한된 메모리)
  • 클라우드 비용 절감

튜닝 방법:

  • Heap 크기 줄이기
  • Serial GC (작은 heap 에 적합)

GC 튜닝의 트레이드오프 ⭐

[STW 짧음] ←————————→ [Throughput 높음]
ZGC                      Parallel
저지연                    배치/처리량

모두 만족 불가상황에 맞는 선택.


실용적 튜닝 방법

Step 1: GC 로그 켜기
  -Xlog:gc*:file=gc.log

Step 2: 측정
  - 평균 GC 시간
  - Full GC 빈도
  - Heap 사용 패턴

Step 3: 분석
  - GCViewer, gceasy.io 등 도구

Step 4: 가설 + 변경
  - 알고리즘 변경
  - Heap 크기 조정
  - Young/Old 비율

Step 5: 재측정
  - 효과 검증

ILIC 환경에서의 일반 권장:

# 일반적 백엔드 (Spring Boot)
java -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=200 \
     -Xms2g -Xmx2g \
     -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/var/log/ \
     -Xlog:gc*:file=/var/log/gc.log \
     -jar ilic.jar

핵심 옵션:

  • UseG1GC: G1 GC (Java 9+ 기본)
  • MaxGCPauseMillis=200: 목표 STW 200ms
  • Xms == Xmx: Heap 고정 (재할당 비용 회피)
  • HeapDumpOnOutOfMemoryError: OOM 시 덤프

다음 Unit으로

  • Heap 의 세대 구조 를 학습할 준비 완료
  • Eden, Survivor, Old Generation 의 자세한 동작이 궁금하다
  • 객체의 일생 (Eden → Survivor → Old) 을 만날 준비 완료
profile
Software Developer

0개의 댓글