F-lab Java 1주차 / Phase 5 / Unit 5.3 본격 학습 자료
9-섹션 마스터 프롬프트 형식으로 깊이 파헤친다.선수 지식: Unit 5.2 (Heap의 세대 구조)
다음 Unit: 5.4 — GC 종류와 선택 기준이 Unit의 의미: GC 의 4가지 핵심 알고리즘 비교.
각 알고리즘의 동작 원리, 장단점, 자바가 무엇을 채택했는지.
면접에서 "왜 자바는 Reference Counting 안 쓰나요?" 같은 깊이 있는 질문 대비.
큰 사무실을 청소하는 4가지 방법을 상상해보세요.
장점: 즉각적, 멈춤 없음
단점: 순환 참조 문제 — A 가 B 사용, B 가 A 사용 → 둘 다 안 버려짐 ❌
장점: 순환 참조 해결, 단순
단점: STW, 빈 자리 곳곳 (단편화)
장점: 단편화 해소
단점: 정리 비용, 객체 이동
장점: 효율적
단점: 구조 복잡
→ 이게 4가지 GC 알고리즘. 자바는 방식 4 (Generational) 채택.
"GC 알고리즘은 '쓰레기를 어떻게 식별하고 제거할까' 의 4가지 답이다."
각 알고리즘의 trade-off:
비유 정리:
| 비유 | GC 알고리즘 | 장점 | 단점 |
|---|---|---|---|
| 방문자 카운터 | Reference Counting | 즉시, 멈춤 X | 순환 참조 |
| 포스트잇 표시 | Mark-Sweep | 단순 | 단편화 |
| 표시 + 정리 | Mark-Sweep-Compact | 압축 | 이동 비용 |
| 세대별 청소 | Generational | 효율 | 복잡 |
GC 의 핵심 문제:
"Heap 의 어떤 객체가 사용 중 이고, 어떤 게 쓰레기 인가?"
이 질문에 답하는 방식이 알고리즘 의 차이.
가장 직관적인 방식, 1950년대 LISP에서 처음 등장.
아이디어:
장점:
문제 발견 — 순환 참조 ⚠️ :
A → B → A
A 의 카운트 = 1 (B 가 가리킴)
B 의 카운트 = 1 (A 가 가리킴)
외부에서 A, B 사용 안 해도
서로 가리키는 한 카운트 0 안 됨
→ 메모리 누수 ❌
채택 사례:
shared_ptr)LISP 언어의 John McCarthy 가 1960년에 제안.
아이디어:
장점:
단점:
메모리 그림:
Before:
[A][B][C][D][E][F]
Mark: A, C, E 가 Live
After Sweep:
[A][_][C][_][E][_]
비움 비움 비움
← 단편화
Mark-Sweep 의 단편화를 해결.
아이디어:
장점:
단점:
메모리 그림:
Sweep 후:
[A][_][C][_][E][_]
Compact 후:
[A][C][E][_][_][_]
← 모든 빈 공간 뒤로
약한 세대 가설 (Unit 5.1) 활용.
아이디어:
장점:
단점:
자바는 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 등으로 더 진화.
알고리즘 차이를 모르면 다양한 상황에서 막힙니다.
면접 질문:
"Python 처럼 Reference Counting 쓰면 즉시 회수 가능하지 않나요?"
알고리즘 모르면:
알고리즘 알면:
운영 환경:
Heap 사용량: 70%
그러나 1MB 이상 객체 할당 시 OOM
알고리즘 모르면:
알고리즘 알면:
신규 ILIC 시스템 배포:
알고리즘 모르면:
알고리즘 알면:
Python 개발자가 자바 시작:
"GC 가 자주 멈춰요. Python 은 안 그랬는데?"
알고리즘 모르면:
알고리즘 알면:
"GC 알고리즘의 진화를 설명해주세요"
"Mark-Sweep 의 단점이 뭔가요?"
"왜 자바는 Mark-Sweep-Compact 와 Copying 둘 다 쓰나요?"
알고리즘 모르면:
알고리즘 알면:
관찰: Heap 사용량 계속 증가
GC 후에도 줄지 않음
알고리즘 모르면:
알고리즘 알면:
| 시나리오 | 알고리즘 모르면 | 알고리즘 알면 |
|---|---|---|
| Reference Counting 질문 | 막막함 | 순환 참조 설명 |
| 단편화 OOM | 이해 X | Compact 알고리즘 선택 |
| 운영 GC 선택 | 기본 사용 | 상황별 선택 |
| 언어 비교 | 표면적 | 깊이 있는 비교 |
| 면접 깊이 질문 | 탈락 | 시니어 답변 |
| 메모리 누수 | "GC 문제" | "참조 문제" |
→ 알고리즘 이해는 시니어의 차별화 영역.
모든 객체에 참조 카운트 보유:
객체 A:
- 데이터
- 참조 카운트: 1
참조 추가/제거 시:
Object x = a; // a 의 카운트 +1
x = null; // a 의 카운트 -1
카운트 0 → 즉시 회수.
1. 즉각적 회수
2. STW 없음
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. 카운트 관리 비용
3. 메모리 오버헤드
| 언어 | 사용 |
|---|---|
| Python | Reference Counting + Cycle Detector (보조) |
| Objective-C | ARC (Automatic Reference Counting) |
| Swift | ARC |
| C++ | shared_ptr |
| Java | ❌ 사용 안 함 |
2단계:
Mark 단계:
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. 순환 참조 해결
2. 단순한 알고리즘
1. STW 필요
2. 단편화 발생 ⚠️
After Sweep:
[A][_][C][_][_][F][_][_]
→ 8 byte 빈 공간 5개
→ 그러나 16 byte 객체 할당 불가 (연속 공간 없음)
3. Mark 시간 비례
자바의 GC 는 Mark-Sweep 의 변형:
3단계:
Step 1-2: Mark + Sweep (같음)
After Sweep:
[A][_][C][_][_][F][_][_]
Step 3: Compact
After Compact:
[A][C][F][_][_][_][_][_]
(살아있는 것 앞쪽 모음)
(빈 공간 뒤로)
1. 단편화 해소
2. 메모리 효율
1. Compact 비용
2. STW 길어짐
Old Generation 에서 사용:
약한 세대 가설 활용:
Young 영역: Copying GC
Old 영역: Mark-Sweep-Compact
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
효과:
1. 매우 효율적
2. 단편화 해소
3. 짧은 STW (보통)
1. 구조 복잡
2. 메모리 오버헤드
자바 모든 GC 의 기반:
→ 모두 Generational + 추가 최적화.
| Reference Counting | Mark-Sweep | Mark-Sweep-Compact | Generational | |
|---|---|---|---|---|
| 순환 참조 | ❌ 누수 | ✅ 해결 | ✅ 해결 | ✅ 해결 |
| STW | 없음 | 김 | 매우 김 | 짧음 (Minor) |
| 단편화 | 없음 | 있음 | 없음 (압축) | 없음 (Young 자동) |
| 즉시 회수 | ✅ | X | X | X |
| 구조 | 단순 | 단순 | 보통 | 복잡 |
| 자바 채택 | ❌ | △ (변형) | △ (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 진화.
# 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 시 회수
DFS (Depth-First Search) 사용:
1. GC Root 들을 work-list 에 추가
2. while work-list 비어있지 않음:
a. 객체 하나 꺼냄
b. 마킹
c. 그 객체의 모든 참조를 work-list 에 추가
3. 끝나면 마킹 안 된 것이 Garbage
시간 복잡도:
for each 객체 in Heap:
if 마킹 안 됨:
free(객체)
else:
clear_mark(객체) // 다음 GC 위해
Free List:
객체 이동 + 참조 업데이트:
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
비용:
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 객체 수)
새 객체 생성:
→ 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 (전체)
문제: 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:
// 사용자 코드
oldObject.youngRef = youngObject;
// JVM 내부 (실제 바이트코드)
oldObject.youngRef = youngObject;
markCardAsDirty(oldObject); // ← Write Barrier
→ Generational GC 의 핵심 메커니즘.
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 기반 → 순환 참조 안전.
// ❌ 자바에서 실제 메모리 누수 패턴
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 이라도.
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 회수됨");
}
}
알고리즘별 동작:
// 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
선택 기준:
java -XX:+PrintCommandLineFlags -version
출력 예시:
-XX:InitialHeapSize=...
-XX:+UseG1GC ← 사용 중인 GC
-XX:+UseCompressedClassPointers
...
또는:
java -Xlog:gc*:stdout YourApp
GC 로그에서 알고리즘 이름 확인 가능.
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++ 등에서는 단편화 위험.
# 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
선택 기준:
Q: "자바 GC 알고리즘?"
A: "Reference Counting 같은 거?" ❌
정답: 자바는 Generational + Mark-Sweep-Compact 사용. Reference Counting 사용 안 함.
"순환 참조 만들면 메모리 누수네!"
진실: 자바는 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 이 살아있어 둘 다 누수
"자바는 Mark-Sweep 사용합니다"
정답: Generational + Mark-Sweep-Compact (Old) + Copying (Young).
각 영역마다 다른 알고리즘.
"GC 동안 항상 1초씩 멈춰요"
정답:
GC 알고리즘 + Heap 크기에 따라 다름. ZGC 면 ms 단위.
Q: "Mark-Sweep 단편화는?"
A: "Compact 있어서 OK 아닌가요?" ❌
정답: 순수 Mark-Sweep 은 Compact 없음. 단편화 발생.
자바의 Old GC 는 Mark-Sweep-Compact (압축 추가).
java -jar myapp.jar # 기본 GC 사용
문제:
상황별 권장:
public void process() {
// 처리
System.gc(); // ❌ 권장 X
}
문제:
원칙: GC 는 JVM 이 알아서.
[Unit 5.1: GC 기본 + 약한 세대 가설] ✓
↓
[Unit 5.2: Heap 의 세대 구조] ✓
↓
[Unit 5.3: GC 알고리즘 4가지] ← 지금 여기 ★
↓
[Unit 5.4: GC 종류와 선택 기준] (마지막)
| 시기 | 알고리즘 | 주요 개선 |
|---|---|---|
| 1950s | Reference Counting | 즉시 회수 |
| 1960s | Mark-Sweep | 순환 참조 해결 |
| 1970s | Mark-Sweep-Compact | 단편화 해결 |
| 1980s | Generational | 효율 ↑ |
| 1990s | Concurrent (CMS) | STW 짧음 |
| 2010s | G1 (Region) | 큰 Heap |
| 2010s | ZGC | 매우 짧은 STW |
| 언어 | GC 알고리즘 |
|---|---|
| Java | Generational + Mark-Sweep-Compact + Copying |
| C#/.NET | Generational + Mark-Sweep-Compact (비슷) |
| Go | Concurrent Mark-Sweep (저지연 우선) |
| Python | Reference Counting + Cycle Detector |
| JavaScript (V8) | Generational + Mark-Sweep |
| Ruby | Mark-Sweep-Compact + Generational |
| Rust | GC 없음 (소유권) |
| 질문 | 이 Unit에서의 답 |
|---|---|
| "GC 알고리즘?" | RC, Mark-Sweep, MSC, Generational |
| "왜 자바는 RC 안 씀?" | 순환 참조 + 카운트 비용 |
| "Mark-Sweep 단점?" | 단편화 |
| "자바 GC 의 특징?" | Generational + 영역별 다른 알고리즘 |
| "Copying GC?" | Young 영역에서 사용 |
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 (저지연) 권장.
Q1: 순환 참조가 Reference Counting 에서 왜 문제인지 그림으로 그려보라.
답: 외부 참조가 모두 끊겼는데, 객체끼리 상호 참조 하면 카운트가 0 안 돼서 누수.
상세 시각화:
[Stack]
alice (변수)
bob (변수)
[Heap]
Person "Alice" cnt=1 (alice 가 가리킴)
Person "Bob" cnt=1 (bob 이 가리킴)
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
↑ │
└──────────────────────────┘
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 안 됨
→ 메모리 누수 ❌
핵심 문제:
해결책들:
주기적으로:
- 카운트 0 안 됐지만 도달 불가능한 객체 식별
- 그것들 회수
GC Root 에서 시작
→ alice, bob 객체 도달 불가
→ Mark 안 됨
→ Sweep 시 회수
class Person {
WeakReference<Person> friend; // 약한 참조
// → 카운트 증가 X
// → 순환 참조 발생 X
}
→ 자바는 Mark-Sweep 으로 우아하게 해결.
Q2: Mark-and-Sweep 에서 OOM 이 발생할 수 있는 이유는?
한 줄 답: 단편화 (Fragmentation) 때문에 큰 연속 공간 부족.
상세 설명:
Mark-Sweep 만 사용 시 Compact 단계 없음:
[Heap] (1GB)
[객체A 100MB][객체B 100MB][객체C 100MB][객체D 100MB][객체E 100MB]
... 10개 100MB 객체로 가득
Mark:
A (Live), B (Garbage), C (Live), D (Garbage), E (Live), ...
Sweep:
[A 100MB][_ 100MB][C 100MB][_ 100MB][E 100MB][_ 100MB]...
← 빈 공간들 흩어짐
byte[] big = new byte[300 * 1024 * 1024]; // 300MB
문제:
시각화:
빈 공간들:
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 후:
[차1][차3][차5][차7][차9][_][_][_][_][_]
← 연속된 빈 공간
이제 트럭 주차 가능 ✅
자바의 Old GC = Mark-Sweep-Compact:
→ 자바가 Mark-Sweep-Compact 채택한 이유.
G1 GC 는 Region 기반:
G1 의 Humongous Object: