🎯1주차 Unit 5.3 — GC 알고리즘 4가지

Psj·2026년 5월 8일

F-lab

목록 보기
39/239

🎯 Unit 5.3 — GC 알고리즘 4가지

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

선수 지식: Unit 5.2 (Heap의 세대 구조)
다음 Unit: 5.4 — GC 종류와 선택 기준

이 Unit의 의미: GC 의 4가지 핵심 알고리즘 비교.
각 알고리즘의 동작 원리, 장단점, 자바가 무엇을 채택했는지.
면접에서 "왜 자바는 Reference Counting 안 쓰나요?" 같은 깊이 있는 질문 대비.


🌍 1. 세상 속 비유

4가지 GC 알고리즘 = "4가지 청소 방식"

큰 사무실을 청소하는 4가지 방법을 상상해보세요.

방식 1 — "방문자 카운터" (Reference Counting)

  • 모든 물건에 방문자 수 카운터 부착
  • 누군가 사용 시작 → +1
  • 누군가 사용 끝 → -1
  • 카운터 0 → 즉시 버림

장점: 즉각적, 멈춤 없음
단점: 순환 참조 문제 — A 가 B 사용, B 가 A 사용 → 둘 다 안 버려짐 ❌


방식 2 — "사용 중 표시" (Mark-and-Sweep)

  • 청소 시간에 모든 직원 정지 (STW)
  • 사용 중인 물건에 포스트잇 붙이기 (Mark)
  • 포스트잇 없는 것 모두 버림 (Sweep)
  • 다시 일 시작

장점: 순환 참조 해결, 단순
단점: STW, 빈 자리 곳곳 (단편화)


방식 3 — "사용 중 표시 + 정리" (Mark-Sweep-Compact)

  • Mark-Sweep 의 같은 시작
  • 추가로 남은 물건들을 한쪽으로 정렬
  • 빈 공간 한쪽에 모음

장점: 단편화 해소
단점: 정리 비용, 객체 이동


방식 4 — "세대별 청소" (Generational)

  • 신입 책상 (Eden) 자주 청소
  • 일반 책상 (Survivor) 가끔 청소
  • 임원 책상 (Old) 거의 안 청소
  • 각자 다른 청소 방식

장점: 효율적
단점: 구조 복잡

이게 4가지 GC 알고리즘. 자바는 방식 4 (Generational) 채택.


핵심 한 문장

"GC 알고리즘은 '쓰레기를 어떻게 식별하고 제거할까' 의 4가지 답이다."

각 알고리즘의 trade-off:

  • 순환 참조 처리?
  • STW 길이?
  • 단편화?
  • 효율성?

비유 정리:

비유GC 알고리즘장점단점
방문자 카운터Reference Counting즉시, 멈춤 X순환 참조
포스트잇 표시Mark-Sweep단순단편화
표시 + 정리Mark-Sweep-Compact압축이동 비용
세대별 청소Generational효율복잡

🔥 2. 탄생 배경

"쓰레기를 어떻게 알아낼까?" — 가장 본질적 질문

GC 의 핵심 문제:

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

이 질문에 답하는 방식이 알고리즘 의 차이.


알고리즘 1 — Reference Counting (1950s) ⭐

가장 직관적인 방식, 1950년대 LISP에서 처음 등장.

아이디어:

  • 모든 객체에 참조 카운트 필드 추가
  • 참조 +1 / -1 → 카운트 추적
  • 카운트 0 → 즉시 회수

장점:

  • 즉각적 (0이 되는 순간 회수)
  • STW 없음
  • 단순한 개념

문제 발견 — 순환 참조 ⚠️ :

A → B → A
A 의 카운트 = 1 (B 가 가리킴)
B 의 카운트 = 1 (A 가 가리킴)

외부에서 A, B 사용 안 해도
서로 가리키는 한 카운트 0 안 됨
→ 메모리 누수 ❌

채택 사례:

  • Python (보조 GC 와 함께)
  • Objective-C, Swift (ARC)
  • C++ (shared_ptr)
  • 자바는 채택 X

알고리즘 2 — Mark-Sweep (1960) ⭐

LISP 언어의 John McCarthy 가 1960년에 제안.

아이디어:

  • GC 시 모든 살아있는 객체 마킹 (Mark)
  • 마킹 안 된 객체 모두 회수 (Sweep)

장점:

  • 순환 참조 해결 (Reachability 기반)
  • 단순한 알고리즘
  • 모든 GC 의 토대

단점:

  • STW 필요
  • 단편화 발생

메모리 그림:

Before:
[A][B][C][D][E][F]
Mark: A, C, E 가 Live

After Sweep:
[A][_][C][_][E][_]
   비움    비움    비움
   ← 단편화

알고리즘 3 — Mark-Sweep-Compact

Mark-Sweep 의 단편화를 해결.

아이디어:

  • Mark-Sweep 후 압축 (Compact) 단계 추가
  • 살아있는 객체를 한쪽으로 모음

장점:

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

단점:

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

메모리 그림:

Sweep 후:
[A][_][C][_][E][_]

Compact 후:
[A][C][E][_][_][_]
       ← 모든 빈 공간 뒤로

알고리즘 4 — Generational (1980s) ⭐

약한 세대 가설 (Unit 5.1) 활용.

아이디어:

  • Heap을 Young / Old 분리
  • Young: Copying GC (자주, 빠름)
  • Old: Mark-Sweep-Compact (가끔, 큰 정리)

장점:

  • 매우 효율적
  • 세대별 최적화

단점:

  • 구조 복잡
  • Old → Young 참조 추적 (Card Table)

자바의 선택 — Generational ⭐

자바는 Generational GC 를 기본 채택:

왜?:
1. 약한 세대 가설 의 통찰 활용
2. STW 짧음 (Minor GC)
3. 큰 정리는 가끔만 (Major GC)
4. 현대 모든 자바 GC 가 이 기반

→ Serial, Parallel, CMS, G1, ZGC 모두 Generational + 추가 최적화.


핵심 통찰

"GC 알고리즘의 진화 = '효율 + 안전' 의 추구."

Reference Counting의 직관성 → 순환 참조 문제 발견 → Mark-Sweep 의 안전성 → 단편화 문제 → Mark-Sweep-Compact의 압축 → 모든 객체 똑같이 다루는 비효율 → Generational의 효율.

각 알고리즘은 이전 알고리즘의 한계를 해결. 자바는 가장 진화된 형태인 Generational 채택, 이후 G1, ZGC 등으로 더 진화.


💣 3. 없으면 생기는 문제

알고리즘 차이를 모르면 다양한 상황에서 막힙니다.

시나리오 1: "왜 자바는 Reference Counting 안 쓰나?"

면접 질문:

"Python 처럼 Reference Counting 쓰면 즉시 회수 가능하지 않나요?"

알고리즘 모르면:

  • "음... 자바가 그냥 그렇게 만들었나봐요"
  • → 시니어 자격 의심

알고리즘 알면:

  • "순환 참조 문제 때문입니다."
  • 구체적 예시 + 그림으로 설명
  • "Python 도 Reference Counting 만으론 부족해서 Cycle Detector 보조 사용"
  • → 시니어 인식

시나리오 2: 단편화의 의미 이해 못함

운영 환경:

Heap 사용량: 70%
그러나 1MB 이상 객체 할당 시 OOM

알고리즘 모르면:

  • "Heap 30% 남았는데 왜 OOM?"
  • 이해 불가

알고리즘 알면:

  • "단편화 때문"
  • "Mark-Sweep 후 빈 공간이 작은 조각들로 흩어져 있음"
  • "큰 연속 공간이 부족"
  • → Compact 가 있는 알고리즘 (G1 등) 으로 전환

시나리오 3: ILIC 운영의 GC 알고리즘 선택

신규 ILIC 시스템 배포:

  • Heap: 8GB
  • 평균 응답 100ms
  • 기본 GC 사용 → 가끔 5초 멈춤

알고리즘 모르면:

  • "이상하다... 메모리 늘려?"
  • 근본 해결 X

알고리즘 알면:

  • "Parallel GC 의 Old 영역 Full GC 가 길음"
  • "G1 또는 ZGC 로 전환"
  • "G1 = Region 기반 + 부분 압축, ZGC = 거의 멈춤 없음"

시나리오 4: Python 에서 자바로 전환

Python 개발자가 자바 시작:

"GC 가 자주 멈춰요. Python 은 안 그랬는데?"

알고리즘 모르면:

  • 이유 못 찾음

알고리즘 알면:

  • "Python 은 Reference Counting 위주 → 즉시 회수"
  • "자바는 Generational + Mark-Sweep → 가끔 STW"
  • "각 알고리즘의 trade-off"
  • → 정확한 비교

시나리오 5: 면접 깊이 질문

"GC 알고리즘의 진화를 설명해주세요"
"Mark-Sweep 의 단점이 뭔가요?"
"왜 자바는 Mark-Sweep-Compact 와 Copying 둘 다 쓰나요?"

알고리즘 모르면:

  • 깊이 있는 답 불가

알고리즘 알면:

  • 4가지 알고리즘의 진화 + 장단점
  • 자바의 채택 이유
  • → 시니어 답변

시나리오 6: 메모리 누수 분석

관찰: Heap 사용량 계속 증가
GC 후에도 줄지 않음

알고리즘 모르면:

  • "GC 가 안 동작하나?"

알고리즘 알면:

  • "GC 는 동작하는데 Reachability 가 있는 객체는 못 회수"
  • "참조 끊김 어디서 안 되는지 분석"
  • → static 컬렉션, ThreadLocal 등 의심

알고리즘 이해의 중요성

시나리오알고리즘 모르면알고리즘 알면
Reference Counting 질문막막함순환 참조 설명
단편화 OOM이해 XCompact 알고리즘 선택
운영 GC 선택기본 사용상황별 선택
언어 비교표면적깊이 있는 비교
면접 깊이 질문탈락시니어 답변
메모리 누수"GC 문제""참조 문제"

알고리즘 이해는 시니어의 차별화 영역.


✅ 4. 해결책 — 4가지 알고리즘 정확히

알고리즘 1: Reference Counting ⭐

핵심 동작

모든 객체에 참조 카운트 보유:

객체 A:
  - 데이터
  - 참조 카운트: 1

참조 추가/제거 시:

Object x = a;  // a 의 카운트 +1
x = null;      // a 의 카운트 -1

카운트 0 → 즉시 회수.


장점 ⭐

1. 즉각적 회수

  • 카운트 0 되는 순간 즉시 메모리 해제
  • 메모리 효율적

2. STW 없음

  • 별도 GC 사이클 없음
  • 사용자 응답에 영향 X

3. 분산된 작업

  • 매 참조 변경 시 조금씩 작업
  • 한 번에 큰 부하 없음

단점 ⭐⭐ (중요)

1. 순환 참조 문제 ⚠️ (자기 점검 Q1)

class Node {
    Node next;
}

Node a = new Node();
Node b = new Node();

a.next = b;  // a → b
b.next = a;  // b → a (순환!)

a = null;  // a 의 외부 참조 끊음
b = null;  // b 의 외부 참조 끊음

// 그러나:
// a 의 카운트: 1 (b.next 가 가리킴)
// b 의 카운트: 1 (a.next 가 가리킴)
// → 카운트 0 안 됨 → 메모리 누수 ❌

그림:

[Stack] (외부 참조 없음)

[Heap]
  Node A ─→ Node B
       ↑      │
       └──────┘
       
A 의 카운트: 1 (B 의 next)
B 의 카운트: 1 (A 의 next)
→ 둘 다 회수 X

2. 카운트 관리 비용

  • 매 참조 변경마다 +1 / -1
  • 멀티스레드 환경에서 동기화 필요

3. 메모리 오버헤드

  • 모든 객체에 카운트 필드 추가
  • 8 byte 객체에 4 byte 카운트?

채택 언어

언어사용
PythonReference Counting + Cycle Detector (보조)
Objective-CARC (Automatic Reference Counting)
SwiftARC
C++shared_ptr
Java❌ 사용 안 함

알고리즘 2: Mark-and-Sweep ⭐

핵심 동작

2단계:

Mark 단계:

  • GC Root 부터 시작
  • 도달 가능한 객체 모두 마킹

Sweep 단계:

  • 마킹 안 된 객체 모두 회수
[Stack]
  ref1, ref2

[Heap]
  Object A ←─── ref1     (Mark)
  Object B               (X)
  Object C ←─── ref2     (Mark)
  Object D ←─ A          (Mark)
  Object E               (X)

동작 그림

Step 1: Initial

[Heap]
  A B C D E F G H
  ↑   ↑       ↑
  GC Root 들

Step 2: Mark

[Heap]
  A* B C* D E F* G H
  *=마킹됨 (Live)
  
GC Root 에서 도달 가능: A, C, F

Step 3: Sweep

[Heap]
  A _ C _ _ F _ _
       (B,D,E,G,H 회수)

장점 ⭐

1. 순환 참조 해결

  • Reachability 기반 → 순환은 외부 참조 없으면 회수됨
  • Reference Counting 의 가장 큰 단점 해결

2. 단순한 알고리즘

  • 쉬운 구현
  • 모든 GC 의 토대

단점 ⭐

1. STW 필요

  • 마킹 중 객체 변경 X
  • 모든 Application Thread 정지

2. 단편화 발생 ⚠️

  • Sweep 후 빈 공간이 곳곳에
  • 큰 객체 할당 어려움
After Sweep:
[A][_][C][_][_][F][_][_]

→ 8 byte 빈 공간 5개
→ 그러나 16 byte 객체 할당 불가 (연속 공간 없음)

3. Mark 시간 비례

  • 객체 수 많을수록 Mark 시간 증가

자바에서의 사용

자바의 GC 는 Mark-Sweep 의 변형:

  • Old Generation 의 GC 에 사용
  • Compact 가 추가된 형태 (Mark-Sweep-Compact)

알고리즘 3: Mark-Sweep-Compact ⭐

핵심 동작

3단계:

  1. Mark: 살아있는 객체 마킹
  2. Sweep: 죽은 객체 회수
  3. Compact: 살아있는 객체를 한쪽으로 압축

동작 그림

Step 1-2: Mark + Sweep (같음)

After Sweep:
[A][_][C][_][_][F][_][_]

Step 3: Compact

After Compact:
[A][C][F][_][_][_][_][_]
       (살아있는 것 앞쪽 모음)
       (빈 공간 뒤로)

장점 ⭐

1. 단편화 해소

  • 빈 공간이 한곳에 모임
  • 큰 객체도 할당 가능

2. 메모리 효율

  • 사용 중 영역 축소

단점 ⭐

1. Compact 비용

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

2. STW 길어짐

  • Mark + Sweep + Compact = 더 긴 STW

자바에서의 사용

Old Generation 에서 사용:

  • Old GC = Mark-Sweep-Compact (전통)
  • G1 GC 도 Old 영역에 부분 적용

알고리즘 4: Generational ⭐⭐⭐ (자바의 실제)

핵심 동작

약한 세대 가설 활용:

  • Young 영역 (Eden + Survivor)
  • Old 영역
  • 각자 다른 알고리즘

Young 영역: Copying GC

  • Eden + From → To 로 복사
  • 자동 압축
  • 매우 빠름

Old 영역: Mark-Sweep-Compact

  • 가끔 발생
  • STW 길지만 자주 X

Copying GC 의 동작 (Young 영역)

Before Minor GC:
[Eden + From]
  A B C D E (B, D 는 Garbage)

[To]
  비어있음

Copying:
  살아있는 A, C, E 를 To 로 복사

After:
[Eden + From]
  비움

[To]
  A C E

효과:

  • 자동 압축
  • 단편화 X
  • 빠름 (살아있는 것 적음)

장점 ⭐

1. 매우 효율적

  • 약한 세대 가설 활용
  • Young 자주 GC, 빠름
  • Old 가끔 GC, 큼

2. 단편화 해소

  • Young: Copying 으로 자동
  • Old: Compact

3. 짧은 STW (보통)

  • Minor GC: ~10ms
  • Major GC: 가끔만

단점 ⭐

1. 구조 복잡

  • Eden + Survivor 0/1 + Old
  • Promotion 로직
  • Card Table 필요

2. 메모리 오버헤드

  • Survivor 영역 (실제 사용은 절반)
  • Card Table 자체

자바의 채택 ⭐

자바 모든 GC 의 기반:

  • Serial GC (가장 단순)
  • Parallel GC (멀티 스레드)
  • CMS (Concurrent)
  • G1 (Region)
  • ZGC (저지연)

→ 모두 Generational + 추가 최적화.


4가지 알고리즘 비교 표 ⭐⭐ (면접 답변 핵심)

Reference CountingMark-SweepMark-Sweep-CompactGenerational
순환 참조❌ 누수✅ 해결✅ 해결✅ 해결
STW없음매우 김짧음 (Minor)
단편화없음있음없음 (압축)없음 (Young 자동)
즉시 회수XXX
구조단순단순보통복잡
자바 채택△ (변형)△ (Old 영역)

알고리즘 진화의 흐름

1. Reference Counting (1950s)
   ↓ 순환 참조 문제 발견
   
2. Mark-Sweep (1960s)
   ↓ 단편화 문제 발견
   
3. Mark-Sweep-Compact (1970s)
   ↓ 모든 객체 같게 다루는 비효율
   
4. Generational (1980s) ⭐
   ↓ 자바 채택 (1995)
   ↓ 더 발전
   
5. G1, ZGC, Shenandoah (2010s+)
   ↓ Region 기반, Concurrent

알고리즘 진화 = GC 진화.


🏗️ 5. 내부 동작 원리

Reference Counting 의 자세한 메커니즘

# Python 예시 (자바 아님, 이해용)
a = SomeObject()  # a 의 카운트: 1
b = a              # 카운트: 2
a = None           # 카운트: 1
b = None           # 카운트: 0 → 즉시 회수

JVM 이라면 (가상):

// 의사 코드 — 자바는 이렇게 안 함
class Object {
    int referenceCount;
    
    void onReferenceAdded() { referenceCount++; }
    void onReferenceRemoved() {
        referenceCount--;
        if (referenceCount == 0) {
            free(this);  // 즉시 해제
        }
    }
}

모든 참조 변경 시 카운트 갱신 — 비용 높음.


순환 참조의 정확한 문제

class Person {
    Person friend;  // 친구 참조
}

// 시나리오
Person alice = new Person();   // alice: cnt=1 (외부 변수)
Person bob = new Person();     // bob: cnt=1

alice.friend = bob;  // bob: cnt=2
bob.friend = alice;  // alice: cnt=2

alice = null;  // alice 객체: cnt=1 (bob.friend 가 가리킴)
bob = null;    // bob 객체: cnt=1 (alice.friend 가 가리킴)

// 외부 참조 모두 끊겼는데
// 서로 1씩 카운트 → 영원히 회수 X

시각화:

[Stack] (비어있음 — 외부 참조 X)

[Heap]
  alice 객체 (cnt=1) ──→ bob 객체 (cnt=1)
       ↑                    │
       └────────────────────┘
       (cycle)

누수.

Mark-Sweep 이라면 해결:

GC Root: 외부 참조 없음
→ Reachability: alice, bob 둘 다 도달 불가
→ Sweep 시 회수

Mark 의 자세한 알고리즘

DFS (Depth-First Search) 사용:

1. GC Root 들을 work-list 에 추가
2. while work-list 비어있지 않음:
   a. 객체 하나 꺼냄
   b. 마킹
   c. 그 객체의 모든 참조를 work-list 에 추가
3. 끝나면 마킹 안 된 것이 Garbage

시간 복잡도:

  • 객체 수 N
  • 참조 수 E
  • O(N + E)

Sweep 의 자세한 알고리즘

for each 객체 in Heap:
    if 마킹 안 됨:
        free(객체)
    else:
        clear_mark(객체)  // 다음 GC 위해

Free List:

  • 회수된 메모리를 추적
  • 다음 할당 시 사용

Compact 의 자세한 메커니즘

객체 이동 + 참조 업데이트:

Step 1: 새 위치 계산
  A: 0x1000 → 0x1000 (그대로)
  C: 0x2000 → 0x1100 (앞으로 이동)
  F: 0x3000 → 0x1200 (앞으로 이동)

Step 2: 객체 복사
  C 의 데이터 → 0x1100
  F 의 데이터 → 0x1200

Step 3: 모든 참조 업데이트
  A 가 C 를 참조 → 0x2000 → 0x1100
  외부에서 F 참조 → 0x3000 → 0x1200

비용:

  • 객체 복사
  • 참조 추적 + 업데이트
  • → STW 길어짐

Copying GC 의 효율성

Young 영역에서 사용:

Eden + From: [A][B][C][D][E]
              ↓ Live: A, C, E

To: 비어있음
              ↓ Copying

To: [A][C][E]
Eden + From: 통째로 비움

왜 빠른가:
1. 살아있는 객체만 복사 (보통 적음)
2. 빈 공간 통째로 비움 (free 호출 없음)
3. 자동 압축 (별도 단계 X)

시간: O(Live 객체 수)


Generational 의 통합 흐름

새 객체 생성:
  → Eden 할당

Eden 가득:
  → Minor GC (Copying)
  → Eden + From → To
  → 살아남은 객체 age +1

Survivor 가득 또는 age = 15:
  → Promotion (Old 로 이동)

Old 가득:
  → Major GC (Mark-Sweep-Compact)
  → 또는 G1 의 Mixed GC

Old + Metaspace 가득:
  → Full GC (전체)

Card Table 의 필요성 ⭐

문제: Old → Young 참조

@Service
public class FareCache {  // Old 에 살아있음
    private List<Fare> recent = new ArrayList<>();  // Young 에 있을 수 있음
}

Minor GC 시 Eden + From 만 보면, Old 의 cache 가 가리키는 Young 객체 를 Live 로 인식 못 함.

해결 — Card Table ⭐ :

[Old]
  Region 0: [Card 0][Card 1][Card 2][Card 3]
                       ↓
                       dirty (Young 참조 있음)

Minor GC:
1. GC Root 스캔
2. dirty 카드만 추가 스캔
3. 마킹

Write Barrier:

  • Old → Young 참조 발생 시 자동 dirty 마킹
  • JVM 이 컴파일러 수준에서 자동 삽입
// 사용자 코드
oldObject.youngRef = youngObject;

// JVM 내부 (실제 바이트코드)
oldObject.youngRef = youngObject;
markCardAsDirty(oldObject);  // ← Write Barrier

Generational GC 의 핵심 메커니즘.


💻 6. 실전 코드 예시

예시 1: 순환 참조 시뮬레이션 (자바에서는 안전)

class Person {
    String name;
    Person friend;
    
    Person(String name) {
        this.name = name;
    }
}

public class CycleDemo {
    public static void main(String[] args) {
        Person alice = new Person("Alice");
        Person bob = new Person("Bob");
        
        alice.friend = bob;
        bob.friend = alice;
        
        // 외부 참조 끊음
        alice = null;
        bob = null;
        
        // GC 발생 시 — Mark-Sweep 이므로 둘 다 회수 ✅
        // (Reference Counting 이었다면 누수)
        
        System.gc();
    }
}

자바 = Mark-Sweep 기반 → 순환 참조 안전.


예시 2: 메모리 누수 — 의외의 패턴

// ❌ 자바에서 실제 메모리 누수 패턴
public class CacheDemo {
    private static final List<Customer> cache = new ArrayList<>();
    
    public static void addCustomer(Customer c) {
        cache.add(c);
        // cache 는 static → GC Root
        // cache 가 c 를 참조 → c 영원히 Live
        // → 누수
    }
}

→ 자바도 참조가 살아있으면 GC 못 함. Mark-Sweep 이라도.


예시 3: GC 알고리즘별 시각화

public class GCVisualizer {
    
    // 시나리오: 100개 객체 생성, 절반은 살리기
    public static void main(String[] args) {
        List<byte[]> survivors = new ArrayList<>();
        
        for (int i = 0; i < 100; i++) {
            byte[] data = new byte[1024 * 100];  // 100KB
            
            if (i % 2 == 0) {
                survivors.add(data);  // 절반만 살림
            }
            // 나머지는 메서드 종료 시 Garbage
        }
        
        System.out.println("실제 살아있는 객체: 50");
        System.out.println("총 생성 객체: 100");
        // → 50 회수 필요
        
        System.gc();
        System.out.println("GC 후 → 50 회수됨");
    }
}

알고리즘별 동작:

  • Reference Counting: 즉시 50 회수 (참조 없어진 순간)
  • Mark-Sweep: GC 시 50 마킹 + 50 sweep
  • Mark-Sweep-Compact: Sweep + 50 살아있는 것 압축
  • Generational: Eden 의 100개 → Minor GC → 50 만 Survivor 로

예시 4: ILIC 의 GC 알고리즘 선택

// ILIC 운영 환경 — JVM 옵션
// -XX:+UseSerialGC      // Serial (단일 스레드)
// -XX:+UseParallelGC    // Parallel (멀티 스레드)
// -XX:+UseG1GC          // G1 (Region 기반) — Java 9+ 기본
// -XX:+UseZGC           // ZGC (저지연)

// 권장:
java -XX:+UseG1GC \
     -Xms2g -Xmx2g \
     -XX:MaxGCPauseMillis=200 \
     -Xlog:gc*:file=gc.log \
     -jar ilic.jar

선택 기준:

  • 작은 Heap, 단일 CPU: Serial
  • 처리량 우선 (배치): Parallel
  • 일반 백엔드: G1
  • 저지연 필수 (대용량): ZGC

예시 5: GC 로그에서 알고리즘 확인

java -XX:+PrintCommandLineFlags -version

출력 예시:

-XX:InitialHeapSize=...
-XX:+UseG1GC                ← 사용 중인 GC
-XX:+UseCompressedClassPointers
...

또는:

java -Xlog:gc*:stdout YourApp

GC 로그에서 알고리즘 이름 확인 가능.


예시 6: 단편화 실험

public class FragmentationDemo {
    public static void main(String[] args) {
        List<byte[]> arrays = new ArrayList<>();
        
        // 100개 작은 배열 할당
        for (int i = 0; i < 100; i++) {
            arrays.add(new byte[1024 * 100]);  // 100KB
        }
        
        // 짝수 인덱스만 살림 (홀수는 Garbage 대상)
        for (int i = 1; i < 100; i += 2) {
            arrays.set(i, null);
        }
        
        System.gc();  // GC 발생
        
        // Mark-Sweep 만 했다면:
        // 살아있는 것: [A][_][C][_][E][_][G][_]...
        // → 단편화 ⚠️
        
        // Mark-Sweep-Compact 후:
        // [A][C][E][G][_][_][_][_][_]...
        // → 압축 ✅
        
        // 큰 객체 할당 시도
        try {
            byte[] huge = new byte[1024 * 1024 * 50];  // 50MB
            // Mark-Sweep 만이라면 OOM 가능
            // Compact 후라면 OK
        } catch (OutOfMemoryError e) {
            System.out.println("단편화로 OOM!");
        }
    }
}

자바는 Compact 가 있어 안전. C/C++ 등에서는 단편화 위험.


예시 7: ILIC 의 GC 알고리즘별 성능 비교

# Parallel GC (Java 8 기본)
java -XX:+UseParallelGC -Xmx2g -jar ilic.jar
# 결과: Throughput 높음, Full GC 시 STW 1초

# G1 GC (Java 9+ 기본)
java -XX:+UseG1GC -Xmx2g -jar ilic.jar
# 결과: Throughput 약간 낮음, STW ~200ms

# ZGC (Java 11+)
java -XX:+UseZGC -Xmx2g -jar ilic.jar
# 결과: Throughput 약간 더 낮음, STW ~10ms

선택 기준:

  • API 응답 안정성 → G1 또는 ZGC
  • 배치 처리 → Parallel
  • 사용자 응답 우선 → ZGC

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

실수 1: "자바도 Reference Counting 쓴다"

Q: "자바 GC 알고리즘?"
A: "Reference Counting 같은 거?" ❌

정답: 자바는 Generational + Mark-Sweep-Compact 사용. Reference Counting 사용 안 함.


실수 2: 순환 참조가 자바에서 누수라고 생각

"순환 참조 만들면 메모리 누수네!"

진실: 자바는 Mark-Sweep 기반이라 순환 참조 안전.

// 자바에서는 OK
a.friend = b;
b.friend = a;
a = null;
b = null;
// → GC 시 둘 다 회수

순환 참조가 누수인 경우는 외부에서도 참조 할 때:

static List<Person> all = new ArrayList<>();
all.add(alice);
all.add(bob);
// → all 이 살아있어 둘 다 누수

실수 3: Mark-Sweep 만 사용한다고 생각

"자바는 Mark-Sweep 사용합니다"

정답: Generational + Mark-Sweep-Compact (Old) + Copying (Young).

각 영역마다 다른 알고리즘.


실수 4: STW 가 항상 길다고 생각

"GC 동안 항상 1초씩 멈춰요"

정답:

  • Minor GC: ~10ms
  • Major GC: ~100~500ms
  • Full GC: ~수 초

GC 알고리즘 + Heap 크기에 따라 다름. ZGC 면 ms 단위.


실수 5: Compact 가 모든 GC 에 있다고 생각

Q: "Mark-Sweep 단편화는?"
A: "Compact 있어서 OK 아닌가요?" ❌

정답: 순수 Mark-Sweep 은 Compact 없음. 단편화 발생.

자바의 Old GC 는 Mark-Sweep-Compact (압축 추가).


실수 6: GC 알고리즘 무시 = 운영 사고

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

문제:

  • Java 8 기본: Parallel (처리량 우선, 응답성 X)
  • Java 9+ 기본: G1 (균형)

상황별 권장:

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

실수 7: System.gc() 로 GC 강제

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

문제:

  • JVM 이 무시할 수도 있음
  • Full GC → 긴 STW
  • 성능 영향

원칙: GC 는 JVM 이 알아서.


🔗 8. 연관 개념 맵

Phase 5 (GC) 내 흐름

[Unit 5.1: GC 기본 + 약한 세대 가설] ✓
        ↓
[Unit 5.2: Heap 의 세대 구조] ✓
        ↓
[Unit 5.3: GC 알고리즘 4가지] ← 지금 여기 ★
        ↓
[Unit 5.4: GC 종류와 선택 기준] (마지막)

알고리즘 진화 정리

시기알고리즘주요 개선
1950sReference Counting즉시 회수
1960sMark-Sweep순환 참조 해결
1970sMark-Sweep-Compact단편화 해결
1980sGenerational효율 ↑
1990sConcurrent (CMS)STW 짧음
2010sG1 (Region)큰 Heap
2010sZGC매우 짧은 STW

다른 언어와의 비교

언어GC 알고리즘
JavaGenerational + Mark-Sweep-Compact + Copying
C#/.NETGenerational + Mark-Sweep-Compact (비슷)
GoConcurrent Mark-Sweep (저지연 우선)
PythonReference Counting + Cycle Detector
JavaScript (V8)Generational + Mark-Sweep
RubyMark-Sweep-Compact + Generational
RustGC 없음 (소유권)

면접 단골 질문 매핑

질문이 Unit에서의 답
"GC 알고리즘?"RC, Mark-Sweep, MSC, Generational
"왜 자바는 RC 안 씀?"순환 참조 + 카운트 비용
"Mark-Sweep 단점?"단편화
"자바 GC 의 특징?"Generational + 영역별 다른 알고리즘
"Copying GC?"Young 영역에서 사용

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

1️⃣ GC 알고리즘은 4가지 — Reference Counting, Mark-Sweep, Mark-Sweep-Compact, Generational.

Reference Counting 은 참조 카운트로 즉시 회수, 순환 참조 시 누수. Mark-Sweep 은 GC Root 추적으로 마킹 후 회수, 단편화 발생. Mark-Sweep-Compact 은 압축 추가로 단편화 해소. Generational 은 약한 세대 가설로 영역별 다른 알고리즘 적용.

2️⃣ 자바는 Generational + 영역별 다른 알고리즘을 사용한다.

Young 영역 (Eden + Survivor) 은 Copying GC — Eden + From 의 살아있는 객체를 To 로 복사, 자동 압축. Old 영역은 Mark-Sweep-Compact — 마킹 + 회수 + 압축. Reference Counting 은 사용 X (순환 참조 + 카운트 비용 때문). Card Table + Write Barrier 로 Old → Young 참조 추적.

3️⃣ 알고리즘 선택 = 효율 / 단편화 / STW 의 trade-off.

Reference Counting: 즉시 회수, 멈춤 X / 순환 참조 누수. Mark-Sweep: 단순, 안전 / 단편화. Compact: 단편화 해소 / STW 길어짐. Generational: 약한 세대 가설로 균형 / 구조 복잡. Java 의 모든 GC (Serial, Parallel, CMS, G1, ZGC) 가 Generational + 추가 최적화. 운영 시 G1 (균형) 또는 ZGC (저지연) 권장.


🎓 학습 자기 점검

기본 이해

  • 4가지 GC 알고리즘을 나열할 수 있다
  • 각 알고리즘의 핵심 동작을 설명할 수 있다
  • 각 알고리즘의 장단점을 비교할 수 있다
  • 자바가 채택한 알고리즘을 안다 (Generational)

실전 적용

  • 순환 참조가 자바에서 안전한 이유를 설명할 수 있다
  • 단편화 문제를 시각화할 수 있다
  • ILIC 운영에 적합한 GC 알고리즘을 선택할 수 있다
  • GC 로그에서 알고리즘을 식별할 수 있다

면접 대비 (5분 답변)

  • "GC 알고리즘 종류?" 답변 가능
  • "왜 자바는 RC 안 쓰나요?" 답변 가능
  • "Mark-Sweep 의 단점?" 답변 가능
  • "Generational 이 효율적인 이유?" 답변 가능

자기 점검 질문 답변

Q1: 순환 참조가 Reference Counting 에서 왜 문제인지 그림으로 그려보라.

: 외부 참조가 모두 끊겼는데, 객체끼리 상호 참조 하면 카운트가 0 안 돼서 누수.

상세 시각화:

Step 1: 정상 상태

[Stack]
  alice (변수)
  bob (변수)

[Heap]
  Person "Alice"  cnt=1 (alice 가 가리킴)
  Person "Bob"    cnt=1 (bob 이 가리킴)

Step 2: 상호 참조 추가

alice.friend = bob;  // bob: cnt=2 (alice.friend 가 가리킴)
bob.friend = alice;  // alice: cnt=2 (bob.friend 가 가리킴)
[Stack]
  alice ────┐
  bob ──────┼─┐
            │ │
[Heap]      ▼ ▼
  Person "Alice"  cnt=2 ──→ Person "Bob"  cnt=2
       ↑                          │
       └──────────────────────────┘

Step 3: 외부 참조 끊음

alice = null;  // alice 객체: cnt=2-1=1
bob = null;    // bob 객체: cnt=2-1=1
[Stack] (비어있음)

[Heap]
  Person "Alice"  cnt=1 ──→ Person "Bob"  cnt=1
       ↑                          │
       └──────────────────────────┘
       
       두 객체 모두 cnt=1 (서로 가리킴)
       → 영원히 cnt=0 안 됨
       → 메모리 누수 ❌

핵심 문제:

  • 외부에서는 도달 불가 (사용 중 X)
  • 그러나 카운트 0 안 됨
  • → Reference Counting 만으로 처리 불가

해결책들:

1. Cycle Detection (Python 의 보조)

주기적으로:
  - 카운트 0 안 됐지만 도달 불가능한 객체 식별
  - 그것들 회수

2. Mark-Sweep (자바의 선택)

GC Root 에서 시작
→ alice, bob 객체 도달 불가
→ Mark 안 됨
→ Sweep 시 회수

3. Weak Reference (약한 참조)

class Person {
    WeakReference<Person> friend;  // 약한 참조
    // → 카운트 증가 X
    // → 순환 참조 발생 X
}

자바는 Mark-Sweep 으로 우아하게 해결.


Q2: Mark-and-Sweep 에서 OOM 이 발생할 수 있는 이유는?

한 줄 답: 단편화 (Fragmentation) 때문에 큰 연속 공간 부족.

상세 설명:

Mark-Sweep 만 사용 시 Compact 단계 없음:

Step 1: 초기 상태

[Heap] (1GB)
  [객체A 100MB][객체B 100MB][객체C 100MB][객체D 100MB][객체E 100MB]
  ... 10개 100MB 객체로 가득

Step 2: GC 발생 — 짝수 객체만 Live

Mark:
  A (Live), B (Garbage), C (Live), D (Garbage), E (Live), ...

Sweep:
  [A 100MB][_ 100MB][C 100MB][_ 100MB][E 100MB][_ 100MB]...
            ← 빈 공간들 흩어짐

Step 3: 큰 객체 할당 시도

byte[] big = new byte[300 * 1024 * 1024];  // 300MB

문제:

  • Heap 의 빈 공간 총합 = 500MB (충분해 보임)
  • 그러나 연속된 빈 공간 = 100MB 가 최대
  • 300MB 연속 공간 없음
  • OOM ⚠️

시각화:

빈 공간들:
  100MB(B) + 100MB(D) + 100MB(F) + 100MB(H) + 100MB(J) = 500MB

그러나 위치:
  [A][__][C][__][E][__][G][__][I][__]
      ↑ 연속 100MB 만

→ "메모리 부족" 이 아닌 "연속된 공간 부족".


비유 — 주차장의 단편화

주차장 (10대 주차 가능):
  [차1][_][차3][_][차5][_][차7][_][차9][_]

빈 자리: 5개 (절반 비어있음)
그러나 트럭 (3대 자리 필요):
  → 연속 3대 자리 없음
  → 주차 불가 ⚠️

해결책 — Compact

Compact 후:
  [차1][차3][차5][차7][차9][_][_][_][_][_]
                          ← 연속된 빈 공간

이제 트럭 주차 가능 ✅

자바의 실제

자바의 Old GC = Mark-Sweep-Compact:

  • Compact 단계로 단편화 해소
  • 연속된 큰 공간 확보
  • 큰 객체 할당 가능

자바가 Mark-Sweep-Compact 채택한 이유.


G1 의 특수한 경우

G1 GC 는 Region 기반:

  • Heap 을 작은 Region 으로 분할
  • 각 Region 안에서 압축
  • 또는 Region 통째로 회수

G1 의 Humongous Object:

  • Region 의 절반 이상 객체
  • 별도 처리
  • → 단편화 문제 적음

다음 Unit으로

  • GC 종류와 선택 기준 을 학습할 준비 완료
  • Serial, Parallel, CMS, G1, ZGC 의 차이가 궁금하다
  • 운영 환경에서 어떤 GC 를 선택할지 만날 준비 완료
profile
Software Developer

0개의 댓글