✍️ 필사 모드: 가비지 컬렉션 완벽 가이드 — Lisp 1959에서 ZGC까지: Mark-Sweep, Copying, Generational, G1, Shenandoah, Go GC, V8 모든 알고리즘 (2025)
한국어들어가며 — GC는 65년 동안 풀고 있는 문제다
가비지 컬렉션(GC)은 컴퓨터 과학에서 가장 오래된 문제 중 하나이다. 1959년, John McCarthy가 Lisp을 만들면서 "수동 메모리 관리는 너무 어려우니 자동화하자"고 결심한 그 순간부터 시작되었다. 그 결정은 65년이 지난 지금도 활발한 연구 분야이다. ZGC와 Shenandoah가 sub-millisecond pause를 약속하고, Go GC가 백만 코어 데이터 센터의 표준이 되었으며, V8의 Orinoco가 매일 수십억 명이 쓰는 브라우저를 받친다.
이 글은 GC의 모든 것을 다룬다. 1959년의 mark-and-sweep에서 2025년의 ZGC까지, 알고리즘의 진화, 각 알고리즘의 트레이드오프, 그리고 JVM/Go/V8/.NET/Python의 GC 구현체들의 실제 동작 방식까지. 1,400줄에 달하지만, 모든 절은 독립적으로 읽을 수 있다.
이 글은 Linux 메모리 관리 딥다이브와 자매 작품이다. 그 글이 "커널이 메모리를 어떻게 관리하나"였다면, 이 글은 "런타임이 자기 힙을 어떻게 관리하나"이다. 둘이 모이면 "내 객체가 어디 살고 언제 죽는가"라는 질문 전체에 답할 수 있다.
1. GC가 왜 필요한가
1.1 수동 메모리 관리의 어려움
C에서 메모리를 관리하는 것은 어렵다:
char *buf = malloc(1024);
if (something) return; /* 누수! */
free(buf); /* 호출 안 됨 */
수동 관리의 함정:
- 누수(leak): free를 잊음
- dangling pointer: 이미 free된 메모리에 접근
- double free: 같은 메모리를 두 번 해제
- use-after-free: free 후 사용
- 소유권 혼란: 누가 free 책임이 있는지 불명확
이 모든 버그는 디버깅이 어렵고, 보안 취약점의 주요 원인이다. 모던 CVE의 큰 비중이 메모리 안전성 결함이다.
1.2 GC의 약속
GC가 있는 언어에서는 위의 모든 버그가 사라진다. 객체를 만들고, 더 이상 쓰지 않으면, 런타임이 알아서 정리한다.
String s = new String("hello");
// 그냥 잊어버리면 GC가 회수
대신 비용은 있다:
- GC 일시정지(pause) — 회수 작업 중 애플리케이션이 멈출 수 있음
- 메모리 오버헤드 — GC 메타데이터, write barrier 등
- CPU 비용 — 백그라운드 GC 스레드 또는 동시 작업
지난 65년의 GC 역사는 이 비용을 줄이는 노력의 역사이다.
2. 역사 — Lisp 1959에서 ZGC까지
2.1 1959 — John McCarthy의 Mark-and-Sweep
McCarthy는 Lisp을 만들면서 첫 GC를 발명했다. 단순한 두 단계 알고리즘:
- Mark: 루트에서 출발해 도달 가능한 객체에 표시
- Sweep: 표시되지 않은 객체를 모두 free
이 단순한 알고리즘이 모든 후속 GC의 토대가 된다.
2.2 1960 — Reference Counting (George Collins)
각 객체에 참조 카운트를 두고, 카운트가 0이 되면 즉시 회수. 직관적이지만 순환 참조를 처리하지 못한다.
2.3 1970 — Cheney의 Copying GC
C.J. Cheney의 두 페이지짜리 논문이 GC를 한 단계 발전시켰다. 메모리를 두 영역으로 나누고, 살아있는 객체를 한 영역에서 다른 영역으로 복사한다. 죽은 객체는 자동으로 사라진다 — sweep이 필요 없다.
2.4 1984 — David Ungar의 Generational GC
Ungar은 "대부분의 객체는 매우 빨리 죽는다"는 관찰을 토대로 generational GC를 발명했다. 이를 **세대별 가설(generational hypothesis)**이라 한다. young 객체와 old 객체를 분리해 GC하면 young을 자주, old를 가끔 하는 것이 효율적.
2.5 1990s — Incremental과 Parallel
대형 힙이 등장하면서 stop-the-world pause가 문제가 되었다. 두 가지 접근:
- Incremental: GC를 작은 단위로 나눠 애플리케이션과 번갈아 실행
- Parallel: GC 자체를 여러 스레드로 병렬화
2.6 2000s — Concurrent
진정한 동시(concurrent) GC가 등장. 애플리케이션과 GC가 동시에 돈다. Sun의 CMS(Concurrent Mark Sweep)가 첫 상용 동시 GC였다.
2.7 2010s — Region-based와 Pauseless
G1, ZGC, Shenandoah가 등장. 힙을 region으로 나누고, region 단위로 GC. ZGC는 colored pointer로 load barrier를 구현해 거의 0에 가까운 pause time을 달성.
2.8 2020s — Sub-millisecond, 16TB
ZGC가 16TB 힙을 sub-millisecond pause로 관리할 수 있다고 발표. Shenandoah도 비슷한 목표. Go GC도 매년 pause time을 줄여간다. 2025년 시점, "GC가 늦어서 못 쓴다"는 말은 거의 사라졌다.
★ Insight ─────────────────────────────────────
- Generational hypothesis는 경험적 관찰이다: 이론이 아니라 데이터에서 나왔다. 1980년대 Smalltalk와 Lisp 시스템의 메모리 사용 패턴을 분석해보니, 대부분의 객체가 첫 100ms 안에 죽었다. 이는 거의 모든 동적 언어에서 성립한다 (Java, Python, JavaScript, Ruby, C# 모두).
- 왜 Cheney 알고리즘이 우아한가: 두 페이지의 논문에 두 가지 혁신이 들어있다. (1) 살아있는 객체만 만진다는 점, (2) 로컬리티가 자동 개선된다는 점. 옮길 때 같은 객체 그래프 순서대로 복사되므로 캐시 친화적이 된다.
- GC와 RC는 쌍대(dual)이다: GC는 "도달 불가능한 것을 찾는다", RC는 "참조 카운트가 0인 것을 찾는다". David Bacon의 2004년 논문 "A Unified Theory of Garbage Collection"이 둘이 같은 알고리즘의 두 극단임을 증명했다. 모던 GC는 두 접근의 하이브리드를 자주 쓴다.
─────────────────────────────────────────────────
3. GC의 기본 — root, reachability, liveness
3.1 reachability == liveness 가정
GC는 "메모리가 살아있다"를 직접 알 수 없다. 미래에 객체가 사용될지 예측할 수 없기 때문이다 (정지 문제). 그래서 reachability를 liveness의 근사로 쓴다 — 루트에서 참조 체인으로 도달 가능한 객체는 살아있다고 간주.
이는 보수적인 근사다. 도달 가능하지만 사실 다시 안 쓸 객체는 회수되지 못한다 (semantic leak). 그러나 안전하다 — 살아있는 객체를 실수로 회수하는 일은 없다.
3.2 루트 (Root Set)
GC의 시작점. 다음을 포함:
- 모든 글로벌 변수 (static field)
- 모든 활성 스레드의 스택과 레지스터
- 일부 native 코드의 핸들 (JNI 등)
- 일부 cross-VM 참조
루트는 GC가 결정적으로 알 수 있는 "아직 사용 중"의 집합이다. 루트에서 출발해 참조 체인을 따라가면 살아있는 객체 전체에 도달.
3.3 객체 그래프
객체와 객체 간 참조는 그래프를 이룬다. GC는 이 그래프 위에서 도달 가능성 분석을 한다.
[root] → A → B → C
↓
D
[root] → E
A, B, C, D, E 모두 도달 가능. 다른 모든 객체는 garbage.
3.4 GC의 두 가지 큰 질문
- 무엇이 garbage인가? — reachability 분석
- garbage를 어떻게 회수하나? — sweep, copy, compact 등
각 GC 알고리즘은 이 두 질문에 다른 답을 한다.
4. Mark-Sweep — 가장 단순한 GC
4.1 두 단계
- Mark Phase: 루트에서 출발해 DFS/BFS로 그래프를 탐색. 도달 가능한 객체에 mark bit를 켠다.
- Sweep Phase: 힙 전체를 순회. mark bit가 꺼진 객체를 free list에 추가.
def mark(obj):
if obj.marked:
return
obj.marked = True
for child in obj.references:
mark(child)
def sweep():
for obj in heap:
if not obj.marked:
free(obj)
else:
obj.marked = False # 다음 GC를 위해 리셋
def collect():
for root in roots:
mark(root)
sweep()
4.2 Tri-Color Invariant
동시(concurrent) GC를 위해서는 객체를 세 색으로 표시한다:
- 흰색(white): 아직 방문 안 함 — garbage 후보
- 회색(grey): 방문했지만 자식들은 아직 방문 안 함
- 검은색(black): 자기와 자식 모두 방문 완료
GC는 회색 집합을 처리하는 워크리스트로 진행. 회색이 비면 모든 검은색은 살아있고, 모든 흰색은 garbage.
핵심 invariant: 검은색 객체는 흰색 객체를 직접 가리킬 수 없다. 만약 가리킨다면 흰색은 회색을 거치지 않고 GC 후 사라진다 — buggy.
4.3 Write Barrier — Tri-Color 보호
동시 GC 중에 애플리케이션이 객체 그래프를 수정하면 invariant가 깨질 수 있다. 이를 막는 것이 write barrier. 객체에 쓸 때 GC에게 알린다:
// pseudo: write barrier
void write_field(Object *obj, int field, Object *new_value) {
if (gc_in_progress) {
if (obj.color == BLACK && new_value.color == WHITE) {
new_value.color = GREY; // 흰색을 회색으로 승격
grey_set.add(new_value);
}
}
obj.fields[field] = new_value;
}
이를 Dijkstra 스타일 write barrier라 한다. 다른 변종으로 Steele 스타일, Yuasa 스타일이 있다.
4.4 Mark-Sweep의 한계
- 단편화(fragmentation): free 영역들이 sparse하게 흩어짐. 큰 할당이 어려움.
- Sweep 비용: 힙 전체를 순회해야 함 — 큰 힙에서 느림.
이 두 문제를 해결하는 것이 다음 알고리즘들이다.
5. Mark-Compact — 단편화 해결
Mark-Sweep의 sweep 단계를 compact로 바꾼다. 살아있는 객체를 한쪽으로 몰아넣어 빈 공간이 연속되게 한다.
가장 단순한 compact 알고리즘 (LISP 2):
- Mark phase: 도달 가능한 객체에 표시
- 새 주소 계산: 살아있는 객체들의 새 주소를 계산
- 모든 참조 갱신: 옛 주소를 새 주소로 교체
- 객체 이동: 실제로 메모리에서 객체를 옮김
이 방식은 단편화를 완전히 해결하지만 비용이 크다. 모든 살아있는 객체를 만진다 — 큰 힙에서는 매우 느리다.
6. Copying GC — Cheney의 우아한 알고리즘
6.1 핵심 아이디어
힙을 두 반쪽으로 나눈다 — From-space와 To-space. 객체는 항상 From-space에서만 할당된다. GC가 일어나면:
- From-space의 살아있는 객체를 To-space로 복사
- From-space는 그냥 통째로 버림 (no sweep!)
- From과 To의 역할을 swap
6.2 Cheney의 알고리즘
char *to_space_top = to_space_start;
char *scan = to_space_start;
void copy(Object **slot) {
Object *obj = *slot;
if (obj->forwarding) {
*slot = obj->forwarding;
return;
}
// 객체를 To-space로 복사
Object *new_obj = (Object *)to_space_top;
memcpy(new_obj, obj, sizeof(Object));
to_space_top += sizeof(Object);
obj->forwarding = new_obj; // 옛 객체에 forwarding pointer 남김
*slot = new_obj;
}
void collect() {
to_space_top = to_space_start;
scan = to_space_start;
for (Object **root : roots) {
copy(root);
}
// BFS: scan이 top을 따라잡을 때까지
while (scan < to_space_top) {
Object *obj = (Object *)scan;
for (int i = 0; i < obj->n_fields; i++) {
copy(&obj->fields[i]);
}
scan += sizeof(Object);
}
}
핵심 트릭: BFS 큐를 별도로 두지 않는다. To-space 자체가 큐 역할을 한다. scan이 큐의 head, to_space_top이 tail.
6.3 장점
- 빠른 할당: bump pointer만 증가시키면 됨 (Java의 young gen이 이렇게 빠른 이유)
- Compaction이 자동: 복사 결과는 항상 연속 영역
- 죽은 객체 비용 0: 살아있는 것만 만진다
- 로컬리티 향상: 객체 그래프 순서대로 복사되어 캐시 친화적
6.4 단점
- 메모리 사용량 2배: 한쪽만 쓰므로 실제 사용 가능 메모리는 절반
- 큰 객체 복사 비용: 큰 객체가 많으면 비싸짐
- Pinning 어려움: 객체를 옮길 수 없는 경우 (예: native 코드가 가리키는 객체) 처리가 까다로움
이 단점들 때문에 copying GC는 보통 young generation에만 쓴다. Old generation은 mark-sweep 또는 mark-compact.
7. Reference Counting — 즉시 수집
7.1 알고리즘
각 객체에 참조 카운트를 둔다. 새 참조가 생기면 +1, 사라지면 -1. 0이 되면 즉시 free.
typedef struct {
int refcount;
/* ... */
} Object;
void retain(Object *obj) {
if (obj) obj->refcount++;
}
void release(Object *obj) {
if (obj && --obj->refcount == 0) {
for (Object *child : obj->children) {
release(child);
}
free(obj);
}
}
7.2 장점
- 즉시 수집: garbage가 즉시 회수됨. GC pause 없음.
- 로컬 결정: 전역 그래프 분석이 필요 없음.
- 단순함: 구현이 직관적.
7.3 단점
- 순환 참조: A → B → A 같은 사이클은 절대 카운트가 0이 안 됨. 영원히 누수.
- 카운트 갱신 비용: 모든 참조 갱신마다 카운트 조작. 멀티스레드에서는 atomic 연산 필요 — 비싸다.
- 공간 오버헤드: 모든 객체에 카운터 (보통 4-8바이트).
7.4 어디에 쓰이나
- Python: 메인 GC가 reference counting. 별도의 cycle collector가 사이클을 처리.
- Swift/Objective-C: ARC(Automatic Reference Counting). 사이클은 weak reference로 사용자가 명시.
- C++ shared_ptr: 사용자가 명시적으로 RC 사용.
- Perl, PHP: RC 기반.
7.5 RC와 Cycle Collector 결합
Python은 RC + 사이클 수집기를 결합한다:
- 일반 객체: RC로 즉시 수집
- 컨테이너 객체 (list, dict 등): 별도의 generational cycle collector가 주기적으로 사이클 검사
이는 RC의 즉시성과 GC의 완전성을 결합한다.
8. Generational GC — 대부분의 객체는 빨리 죽는다
8.1 Generational Hypothesis
David Ungar의 1984년 관찰: 새로 할당된 객체의 대부분은 매우 빨리 죽는다. 100ms 이내에 90% 이상이 죽는 것이 일반적이다.
이를 활용하면 GC를 매우 효율적으로 만들 수 있다:
- young generation: 자주 GC. 작은 영역, 빠른 처리.
- old generation: 가끔 GC. 큰 영역, 비싼 처리.
대부분의 GC 사이클은 young만 처리하므로 평균 비용이 매우 낮다.
8.2 Young / Old 분리
힙을 두 영역으로 나눈다:
- Young (Nursery): 새 객체가 할당되는 곳. 작음 (일반적으로 힙의 10-30%).
- Old (Tenured): young에서 살아남은 객체가 승격되는 곳. 큼.
Young GC (minor GC): 자주, 빠름. 보통 copying GC. Old GC (major / full GC): 가끔, 느림. 보통 mark-sweep 또는 mark-compact.
8.3 Promotion
객체가 N번의 young GC를 살아남으면 old로 승격(promotion). 또는 young 공간이 부족하면 일부를 강제 승격.
승격 임계값(MaxTenuringThreshold)은 튜닝 파라미터. 너무 작으면 어린 객체가 너무 빨리 old로 가서 부담. 너무 크면 young GC가 비싸짐.
8.4 Cross-Generation 참조 — Write Barrier
문제: young GC를 할 때 old → young 참조도 처리해야 한다 (old의 객체가 young을 가리킬 수 있음). 그러나 old 전체를 스캔하면 minor GC의 빠르기가 사라진다.
해결: write barrier로 cross-generation 참조를 추적.
// pseudo: card marking write barrier
void write_field(Object *obj, int field, Object *new_value) {
obj.fields[field] = new_value;
if (in_old_gen(obj) && in_young_gen(new_value)) {
mark_card(obj);
}
}
Card marking: old gen을 카드(보통 512바이트)로 나누고, 카드별 dirty bit를 둠. write barrier가 dirty bit를 세팅. minor GC는 dirty card만 스캔하면 됨.
8.5 Remembered Set
Card marking의 대안. cross-generation 참조를 명시적으로 기록. G1 GC가 region별 remembered set을 사용한다.
8.6 Generational Hypothesis의 예외
이 가설이 안 맞는 워크로드:
- 캐시 시스템: 객체가 오래 살아남는다. 모든 GC가 비싸짐.
- immutable 함수형: 객체가 자주 생기고 빨리 죽지만 cycle도 없음.
- batch 처리: 큰 객체 그래프가 통째로 살아남다가 통째로 죽음.
이런 워크로드는 generational GC의 이점이 적다. 다른 알고리즘 (G1, ZGC)이 더 적합할 수 있다.
9. 점진/병행/동시 GC — Stop-the-World 회피
9.1 STW의 문제
전통적인 GC는 stop-the-world (STW) — 모든 애플리케이션 스레드를 멈추고 GC가 단독 실행. 작은 힙에서는 견딜 만하지만, 큰 힙에서는 STW pause가 수 초까지 갈 수 있다.
데이터베이스, 게임, 트레이딩 시스템 같은 latency-sensitive 워크로드는 STW를 견딜 수 없다.
9.2 Incremental GC
GC를 작은 단위로 나눠 애플리케이션과 번갈아 실행. 한 번에 모든 객체를 마크하는 대신, 일부만 마크하고 애플리케이션에 control을 돌려준다.
문제: 애플리케이션이 객체 그래프를 수정하면 mark가 잘못될 수 있다. write barrier로 해결.
9.3 Parallel GC
GC 자체를 여러 스레드로 병렬화. 여전히 STW지만 더 빨리 끝남. 멀티 코어 시스템의 표준.
JVM의 -XX:+UseParallelGC (Parallel GC, 옛날 throughput GC).
9.4 Concurrent GC
GC가 애플리케이션과 동시에 실행. 일부 단계는 여전히 STW (initial mark, final remark)지만 대부분의 시간은 동시.
CMS, G1, ZGC, Shenandoah가 모두 concurrent GC.
9.5 단계 분류
전형적인 concurrent GC의 단계:
- Initial mark (STW, 짧음): 루트에서 직접 도달 가능한 객체 마킹
- Concurrent mark (concurrent): 그래프 탐색
- Final remark / Remark (STW, 짧음): concurrent mark 중 변경된 부분 처리
- Concurrent sweep (concurrent): garbage 회수
- (선택) Concurrent compact (concurrent): 단편화 해결
ZGC는 모든 단계가 거의 concurrent. STW는 sub-millisecond.
10. JVM의 GC들
JVM은 가장 다양한 GC 옵션을 가진 런타임이다. 시대별로 추가/제거되었다.
10.1 Serial GC
가장 단순. 단일 스레드 STW GC. Young gen은 copying, old gen은 mark-compact.
활성화: -XX:+UseSerialGC
용도: 작은 힙 (수백 MB 이하), 임베디드, 단일 코어. 모던 서버에서는 거의 안 씀.
10.2 Parallel GC
Serial과 비슷하지만 멀티스레드. STW지만 빠르다. throughput에 최적화.
활성화: -XX:+UseParallelGC (자바 8까지 기본)
용도: throughput이 latency보다 중요한 배치 처리, 비분석 워크로드.
10.3 CMS (Concurrent Mark Sweep)
Sun이 만든 첫 동시 GC. Young은 copying (parallel), old는 concurrent mark-sweep.
활성화: -XX:+UseConcMarkSweepGC
문제: 단편화 처리 못 함. 대형 힙에서 단편화로 인한 full GC가 자주 발생. 그리고 코드가 복잡하고 유지 보수가 어려웠다.
Java 9에서 deprecated, Java 14에서 제거. 짧은 영광이었지만 동시 GC의 길을 닦았다.
10.4 G1 (Garbage-First)
Sun에서 시작해 Oracle이 완성. Java 9부터 기본 GC.
핵심 아이디어:
- 힙을 region (보통 1-32MB)으로 나눔
- 매 GC마다 garbage가 가장 많은 region 우선 처리 (이름의 유래 — "garbage-first")
- young/old 분리는 유지하지만 region 단위로 동적
- pause time goal (
-XX:MaxGCPauseMillis)을 지키도록 region 수를 조정
활성화: -XX:+UseG1GC (Java 9+ 기본)
장점: 큰 힙에서 일관된 pause time. 단점: ZGC/Shenandoah보다는 pause가 길음 (수십 ms 정도).
10.5 ZGC
Oracle의 차세대 GC. Java 11에서 실험적 도입, Java 15에서 production 등급.
핵심 아이디어:
- Colored pointers: 64비트 포인터의 상위 비트를 GC 메타데이터로 사용 (load barrier로 객체 액세스마다 검사)
- Concurrent everything: 거의 모든 단계가 concurrent
- Region-based: G1처럼 region 단위
- Sub-millisecond pause: 어떤 힙 크기에서도 pause < 1ms
활성화: -XX:+UseZGC
용도: 대형 힙 + latency 민감 + 멀티 코어. 데이터베이스, 메시징 시스템, 분석 엔진에 매우 적합.
다음 절에서 자세히.
10.6 Shenandoah
Red Hat의 차세대 GC. ZGC와 같은 시기에 등장, 같은 목표 (sub-ms pause).
핵심 차이: load barrier 대신 Brooks pointer (이후 load reference barrier로 진화). 모든 객체에 forwarding pointer 슬롯을 둔다.
활성화: -XX:+UseShenandoahGC (OpenJDK 12+)
ZGC와 Shenandoah는 거의 같은 워크로드를 다룬다. 차이는 미세하다 — 둘 다 sub-ms pause를 약속하고 둘 다 대형 힙에 좋다.
10.7 Epsilon GC
"아무것도 안 하는" GC. 메모리 할당만 하고 회수는 안 한다. 힙이 다 차면 OOM.
활성화: -XX:+UseEpsilonGC -XX:+UnlockExperimentalVMOptions
용도:
- GC 비용 측정 (Epsilon 대비 다른 GC의 차이)
- 짧은 수명의 batch job
- 메모리 leak 디버깅 (Epsilon으로 돌리면 leak이 빠르게 OOM으로 드러남)
11. ZGC 깊이 있게
11.1 Colored Pointers
ZGC의 핵심 트릭. 64비트 포인터 중 상위 4비트를 GC 메타데이터로 사용:
+----+-----+----+----+-----+
| reserved | f | r | m1 | m0 | address (42 bits) |
+----+-----+----+----+-----+
f= finalizabler= remappedm0/m1= marked (두 GC 사이클 구분)
같은 객체를 가리키는 포인터가 4가지 색을 가질 수 있다. CPU에는 영향이 없다 (멀티매핑으로 같은 물리 페이지를 4개 가상 주소에서 보이게 함).
11.2 Load Barrier
객체를 읽을 때마다 작은 검사 코드가 실행된다:
Object *load_barrier(Object **slot) {
Object *obj = *slot;
if (color(obj) != current_good_color) {
obj = fixup(obj, slot); // 슬랭 패스
}
return obj;
}
대부분의 경우 색이 맞아서 한 비트 비교로 끝난다 — 매우 빠르다 (사이클 단위). 색이 틀리면 slow path로 가서 객체를 새 위치로 옮기고 슬롯을 갱신한다.
이 메커니즘 덕분에 ZGC는 객체를 옮기는 동안에도 애플리케이션이 동시에 돌 수 있다. STW가 거의 필요 없다.
11.3 ZGC의 사이클
- Mark Start (STW, 매우 짧음): 루트만 처리.
- Concurrent Mark: load barrier로 점진적 마크 진행.
- Mark End (STW): 마크 마무리.
- Concurrent Process: weak references 처리.
- Concurrent Relocate: 살아있는 객체를 새 region으로 옮김.
- (다음 GC까지 자동으로 forwarding이 정리됨)
11.4 ZGC의 한계
- Throughput 약간 손해: load barrier 비용이 있음 (약 5%).
- 메모리 오버헤드: 멀티매핑 때문에 가상 주소 공간을 더 씀.
- 압축 OOP 불가: colored pointer 때문에 64비트 포인터를 그대로 써야 함.
대형 힙(수십 GB)에서는 이 손해가 무의미해진다. 작은 힙에서는 G1이나 Parallel이 더 빠를 수 있다.
12. Shenandoah
12.1 Brooks Pointer
ZGC가 colored pointer를 쓴다면, Shenandoah는 Brooks pointer를 썼다 (지금은 진화). 모든 객체의 헤더에 8바이트의 forwarding 슬롯이 있다. 평소에는 자기 자신을 가리키지만, GC가 객체를 옮기면 이 슬롯이 새 위치를 가리킨다.
객체에 접근할 때:
Object *real = obj->forwarding;
real->field = 42; // 항상 forwarding을 거침
이 indirection이 약간의 비용이지만, 동시 객체 이동이 가능해진다.
12.2 Load Reference Barrier
최근 Shenandoah는 Brooks pointer 대신 load reference barrier를 쓴다 (ZGC와 비슷). 객체 헤더 슬롯이 사라져서 메모리 사용이 줄었다.
12.3 Concurrent Compaction
Shenandoah의 강점: compaction까지 concurrent. 다른 GC들이 STW에서 compaction을 하는 반면, Shenandoah는 compaction조차 동시에. 이는 매우 큰 힙(테라바이트)에서 결정적인 차이.
13. Go GC
13.1 디자인 철학
Go는 처음부터 "단순하고 일관된 latency"를 목표로 했다. throughput보다 pause time을 우선.
Go GC는 concurrent tri-color mark-sweep이다. 세대별이 아니다 — Go 팀은 generational GC의 복잡성을 피하기로 했다.
13.2 Tri-Color 모델
위에서 본 흰/회/검 모델 그대로. Write barrier로 invariant 유지.
Go의 write barrier는 hybrid Yuasa-Dijkstra 스타일. 이는 매우 빠르다 (인라인 가능한 작은 코드).
13.3 STW 단계
Go GC도 STW 단계가 있지만 매우 짧다:
- Sweep Termination (STW, 짧음): 이전 GC의 sweep 마무리.
- Mark Setup (STW, 짧음): write barrier 켜기.
- Concurrent Mark: 메인 마크 단계.
- Mark Termination (STW, 짧음): 마크 마무리.
- Sweep (concurrent): garbage 회수.
각 STW는 보통 1ms 이하. Go 1.5에서 약 10ms였던 것이 1.20쯤에는 sub-millisecond까지 줄었다.
13.4 GOGC 튜닝
Go GC의 트리거는 GOGC 환경 변수로 조정한다. 기본 100은 "이전 GC 후 살아남은 메모리의 100% 만큼 새로 할당되면 GC 시작"을 의미한다.
GOGC=200 ./my-app # GC를 덜 자주 — 메모리 더 쓰지만 throughput 높음
GOGC=50 ./my-app # GC를 더 자주 — 메모리 덜 쓰지만 GC 비용 높음
GOGC=off ./my-app # GC 완전 비활성화 (테스트용)
13.5 Pacer
Go GC의 핵심: pacer. 다음 GC 시작 시점을 계산해서 "GC가 끝났을 때 마침 메모리가 다 찼다"가 되도록 한다. 너무 일찍 시작하면 CPU 낭비, 너무 늦게 시작하면 메모리 부족.
이 pacer는 PI 컨트롤러처럼 동작한다. 측정값과 목표값의 차이를 보고 다음 trigger를 조정.
13.6 GOMEMLIMIT
Go 1.19부터 GOMEMLIMIT 환경 변수로 soft 메모리 한도를 설정할 수 있다. 컨테이너의 memory.max와 잘 어울린다.
GOMEMLIMIT=500MiB ./my-app
이 한도에 가까워지면 GC가 더 적극적으로 돈다. OOM kill을 회피하는 좋은 방법.
★ Insight ─────────────────────────────────────
- Go가 generational GC를 거부한 이유: Go 팀은 단순성을 위해 generational의 복잡성을 거부했다. 그러나 그 결과 대형 힙에서 G1/ZGC보다 더 많은 CPU를 쓴다. 트레이드오프가 명확하다 — 단순성 vs 대형 힙 효율.
- Pacer는 제어 이론의 교과서적 응용: GC trigger를 결정하는 것은 PI 컨트롤러 문제. Go는 이를 명시적으로 받아들이고 pacer를 그렇게 구현했다. 다른 GC들도 비슷한 것이 있지만 Go만큼 명시적이지는 않다.
- GOMEMLIMIT은 컨테이너 친화적: 컨테이너의 memory.max를 그대로 GOMEMLIMIT에 넘기면 OOM이 거의 사라진다. 이는 모던 클라우드 환경에서 매우 중요한 진화이다.
─────────────────────────────────────────────────
14. .NET GC
14.1 디자인
.NET GC는 세대별, parallel + concurrent이다. 세 세대를 가진다:
- Gen 0: 새 객체. 가장 자주 수집.
- Gen 1: gen 0에서 살아남은 것.
- Gen 2: gen 1에서 살아남은 것. old generation 격.
Gen 0/1은 빠르고, Gen 2는 비싸다.
14.2 LOH (Large Object Heap)
큰 객체 (85KB 이상)는 별도의 LOH에 할당된다. LOH는 compaction이 거의 안 일어난다 (옮기는 비용이 크므로). 단편화 문제가 있다 — 큰 객체를 자주 생성/해제하는 워크로드에서 골치.
.NET 4.5.1부터 명시적 LOH compaction이 가능 (GCSettings.LargeObjectHeapCompactionMode).
14.3 Server GC vs Workstation GC
.NET은 두 모드를 가진다:
- Workstation GC: 단일 GC 스레드. 데스크탑 앱.
- Server GC: CPU당 GC 스레드 + heap. 처리량 우선.
<gcServer enabled="true"/>로 활성화. 서버 워크로드는 항상 server GC.
14.4 Background GC
Gen 2 수집을 백그라운드로. 다른 GC와 동시 진행. CMS와 비슷.
15. JavaScript GC (V8)
15.1 V8의 디자인
V8은 generational + concurrent 모델. Chrome과 Node.js의 엔진.
- Young generation (New Space): Cheney 알고리즘 + 두 semi-space. 매우 빠른 minor GC.
- Old generation (Old Space): Mark-Sweep + Mark-Compact. 더 비싼 major GC.
- Large Object Space: 큰 객체 (1MB+)의 별도 영역.
15.2 Scavenger — Young Gen
V8의 young GC는 Cheney의 copying. 매우 빠르다.
[New Space] [From-space | To-space]
alloc here ↑ ↑
| |
scavenge: 살아있는 것 옮김
새 객체는 항상 From에 할당. 가득 차면 scavenge 실행 — 살아있는 객체를 To로 옮기고 swap.
15.3 Orinoco — V8 GC 이름
V8의 모던 GC 시스템의 코드네임. 핵심 기능:
- Concurrent marking: old space의 마크가 백그라운드 스레드에서 진행
- Parallel scavenge: young GC도 멀티 스레드
- Idle-time GC: 브라우저가 idle인 짧은 시간에 GC 실행
- Lazy sweeping: sweep을 점진적으로
15.4 Inline Caching과 GC
V8의 hidden class와 inline cache는 GC와 깊이 얽혀 있다. 객체의 형태가 바뀌면 hidden class가 바뀌고, IC가 invalidate된다. GC는 이 IC를 청소해야 한다.
15.5 Idle-Time GC
브라우저가 사용자 입력을 기다리는 짧은 시간 (수십 ms)에 GC를 조금씩 실행. 사용자가 보기에는 GC가 사라진 것처럼 보임.
15.6 Node.js의 GC 튜닝
node --max-old-space-size=4096 app.js # old gen 한도 4GB
node --max-semi-space-size=128 app.js # young gen 한도 128MB
기본값은 작다 (1.7GB). 큰 메모리를 쓰는 Node.js 앱은 반드시 늘려야 함.
16. Python GC
16.1 Reference Counting + Cycle Collector
Python의 메인 GC는 RC. 모든 객체에 ob_refcnt 필드. 0이 되면 즉시 free.
typedef struct _object {
Py_ssize_t ob_refcnt;
PyObject *ob_type;
} PyObject;
Py_INCREF 매크로가 카운트를 +1, Py_DECREF가 -1.
16.2 Cycle Collector
RC는 사이클을 못 잡는다. Python은 별도의 cycle collector가 컨테이너 객체 (list, dict, class instance 등)에 대해서만 동작한다. 일반 객체 (int, string, tuple 등)는 사이클을 만들 수 없으므로 RC만으로 충분.
Cycle collector는 generational이다. 세 세대:
- Generation 0: 새 컨테이너 객체. 자주 검사.
- Generation 1: 살아남은 것.
- Generation 2: 또 살아남은 것.
각 세대마다 트리거 임계값이 있다 (gc.get_threshold()로 조회).
16.3 GIL과 GC
Python의 GIL(Global Interpreter Lock)은 GC를 단순화한다. RC 갱신이 항상 한 스레드에서만 일어나므로 atomic이 필요 없다. 멀티 스레드 환경에서도 RC 비용이 작다.
GIL이 없는 free-threaded Python (3.13+, 실험적)에서는 RC가 atomic이어야 한다 — 비용이 늘어난다. 그래서 free-threaded Python이 GIL Python보다 single-thread 워크로드에서 약간 느린 이유.
16.4 weakref
Python은 weakref 모듈로 약한 참조를 지원한다. weak reference는 카운트를 증가시키지 않으므로 사이클을 부분적으로 회피할 수 있다. 캐시 구현에 자주 쓰인다.
17. 정밀 vs 보수적 GC
17.1 정밀 GC
GC가 어떤 메모리 위치가 포인터이고 어떤 위치가 정수인지 정확히 안다. 컴파일러가 메타데이터를 생성해서 GC에게 알려준다.
장점: 정확. 모든 garbage를 회수. 단점: 컴파일러와 런타임의 깊은 협력 필요.
JVM, Go, .NET, V8 모두 정밀 GC.
17.2 보수적 GC
GC가 메모리 위치의 의미를 모른다. "이 32비트 값은 포인터일 수도, 정수일 수도 있다." 의심스러우면 안전쪽으로 — 포인터로 간주.
장점: C/C++ 같은 언어에서도 사용 가능. 컴파일러 지원 불필요. 단점: false positive가 있을 수 있다 (실제로는 정수인데 포인터로 간주해서 회수 안 함). 메모리 누수의 원인이 될 수 있음.
대표적: Boehm GC. C/C++용 보수적 GC 라이브러리. Mono(.NET 옛날 구현체), MonoDevelop 등이 사용.
17.3 반-보수적 GC (Mostly-Precise)
스택은 보수적, 힙은 정밀. 컴파일러가 힙 객체에 대한 메타데이터는 만들지만 스택 프레임은 만들지 않음. 절충안.
18. GC 튜닝 — JVM 사례
18.1 어디서 시작하나
JVM GC 튜닝의 첫 단계: 로그를 켠다.
java -Xlog:gc*:file=gc.log:time,uptime:filecount=10,filesize=10M ...
로그를 분석하면:
- GC 빈도
- 평균 pause time
- 각 세대의 점유율
- promotion 비율
18.2 핵심 메트릭
- Pause time p99: 99 percentile GC pause.
- GC throughput: 전체 시간 중 application 시간 비율 (1 - GC 시간).
- Allocation rate: 초당 할당 바이트.
- Promotion rate: young → old 승격 바이트.
18.3 일반적 규칙
- Pause time 목표가 100ms 이하면 G1
- Pause time 목표가 10ms 이하면 ZGC 또는 Shenandoah
- Throughput이 절대 우선이면 Parallel GC
- 힙이 작으면 (< 4GB) Serial 또는 Parallel
18.4 힙 크기
너무 작으면 GC가 자주 발생. 너무 크면 full GC pause가 길어짐 (G1 이전 GC들).
일반적 룰: working set의 2-3배. 측정으로 확인.
18.5 JVM 옵션 예시
대형 데이터베이스 서버 (32GB heap, latency 민감):
java -Xms32g -Xmx32g \
-XX:+UseZGC \
-XX:ZCollectionInterval=10 \
-Xlog:gc*:file=gc.log
처리량 우선 배치 잡 (16GB heap):
java -Xms16g -Xmx16g \
-XX:+UseParallelGC \
-XX:ParallelGCThreads=8
19. GC와 운영체제 메모리 압박
19.1 GC는 OS를 모른다
런타임의 GC는 자기 힙 안에서만 결정을 내린다. OS가 메모리 압박을 받고 있는지는 모른다. 그래서 OS가 swap을 시작할 때 GC가 적극적으로 회수해야 하는데 안 한다 — 더 안 좋아진다.
19.2 cgroup memory.max와의 충돌
컨테이너의 memory.max는 hard limit. JVM이 이를 모르면 그것을 넘는 순간 OOM kill.
JVM은 Java 10부터 cgroup limit을 인식한다 (-XX:+UseContainerSupport, 기본 켜짐). Xmx를 명시 안 하면 cgroup limit의 25-75%로 자동 설정.
Go는 GOMEMLIMIT 환경 변수로 명시 (앞 절 참고).
19.3 PSI 신호
Linux 메모리 글에서 본 PSI(Pressure Stall Information). 모던 런타임은 PSI를 읽어서 OS의 메모리 압박을 인식할 수 있다. ZGC가 일부 이런 통합을 시작했다.
19.4 메모리 압박 시 GC 행동
좋은 GC는 OS의 메모리 압박 신호를 받으면 더 적극적으로 회수해야 한다. 안 좋은 GC는 평소대로 돌다가 OOM을 만난다.
이 영역은 모던 GC 연구의 활발한 토픽이다.
20. GC 비교 표
| GC | 알고리즘 | Pause | Throughput | 힙 크기 | 사용처 |
|---|---|---|---|---|---|
| Serial GC (JVM) | Mark-Compact | 길음 | 낮음 | 작음 | 데스크탑, 임베디드 |
| Parallel GC (JVM) | Parallel STW | 보통 | 높음 | 보통 | 처리량 우선 배치 |
| CMS (JVM, 제거됨) | Concurrent Mark-Sweep | 짧음 | 보통 | 보통 | 옛날 latency |
| G1 (JVM) | Region-based + Concurrent | 보통 (수십 ms) | 보통 | 큼 (4GB+) | 일반 서버 |
| ZGC (JVM) | Colored pointer + Concurrent | 매우 짧음 (sub-ms) | 약간 손해 | 매우 큼 (수십 GB+) | 대형 latency 민감 |
| Shenandoah (JVM) | Load barrier + Concurrent | 매우 짧음 | 약간 손해 | 매우 큼 | 대형 latency 민감 |
| Go GC | Tri-color concurrent mark-sweep | 짧음 | 보통 | 보통 | 일반 |
| .NET Server GC | Generational parallel + concurrent | 보통 | 높음 | 큼 | 일반 서버 |
| V8 Orinoco | Generational + concurrent | 매우 짧음 | 보통 | 보통 | 브라우저, Node |
| CPython GC | RC + cycle collector | 거의 없음 | 보통 | 보통 | Python 일반 |
| Boehm GC | Conservative mark-sweep | 보통 | 보통 | 보통 | C/C++ |
21. 흔한 GC 함정과 디버깅
21.1 메모리 누수 (semantic leak)
GC 언어에서도 누수는 일어난다. "도달 가능하지만 다시 안 쓸" 객체가 누적되면.
흔한 패턴:
- Static 컬렉션: 글로벌 캐시에 객체를 넣고 안 빼는 것.
- Listener 등록: 이벤트 리스너를 등록만 하고 unregister 안 함.
- Long-lived 컨테이너: 큰 list/map이 계속 자라기만 함.
- ThreadLocal 잘못 쓰기: 스레드 풀에서 ThreadLocal을 청소 안 함.
도구:
- JVM: jmap, VisualVM, Eclipse MAT
- Go: pprof memory profile
- Python: tracemalloc
- Node: heap snapshot in Chrome DevTools
21.2 GC pause로 인한 latency 스파이크
p99 latency가 갑자기 튀는데 원인을 모르겠을 때 — GC를 의심.
도구:
- JVM: GC log, JFR (Java Flight Recorder)
- Go: GODEBUG=gctrace=1, pprof
- Node: --trace-gc
21.3 Allocation pressure
GC가 자주 도는 이유는 보통 allocation rate이 높기 때문이다. 객체 풀, 재사용 패턴, escape analysis로 줄일 수 있다.
JVM의 escape analysis: HotSpot의 JIT가 객체가 메서드 밖으로 escape 안 한다고 증명하면 스택에 할당. GC 부담 감소.
21.4 큰 객체 문제
큰 배열이나 string은 LOH(.NET) 또는 humongous region(G1)으로 가서 다른 패턴이 적용된다. 큰 객체의 빈번한 할당/해제는 단편화를 만든다.
해결: 큰 객체를 풀로 재사용. ByteBuffer pool, PooledByteBufAllocator 같은 패턴.
21.5 Finalizer / cleaner의 함정
Java의 finalize, Python의 __del__는 GC가 정리할 때 호출되는 콜백. 그러나:
- 호출 시점 보장 없음
- 두 번째 GC 사이클까지 객체가 살아남음
- 예외 처리가 까다로움
- finalizer queue가 막히면 메모리 누수
대안: try-with-resources (Java), context manager (Python), defer (Go), using (C#).
22. 미래 — GC의 다음 도전
22.1 NUMA 인지 GC
대형 멀티 소켓 시스템에서는 객체가 어느 NUMA 노드에 있는지가 중요하다. 모던 GC는 점차 NUMA 인지로 진화 중. ZGC와 G1이 이미 일부 인식.
22.2 Heterogeneous 메모리
DDR + persistent memory + CXL 메모리. 어떤 객체를 어떤 메모리에 둘지 결정하는 것이 새 문제. 핫 객체는 빠른 메모리, 콜드 객체는 느린 메모리.
22.3 GC와 hardware support
ARM의 MTE(Memory Tagging Extension), Intel의 LAM(Linear Address Masking) 같은 하드웨어 기능이 GC에 활용될 수 있다. Tag-based GC 연구가 활발하다.
22.4 Region-based 메모리 관리 (Rust)
Rust는 GC가 없다. 대신 ownership과 lifetime으로 컴파일 타임에 메모리를 관리한다. 이는 GC의 대안이며, 고성능/시스템 코드에 매력적이다.
그러나 Rust도 일부 패턴 (사이클이 필요한 자료구조)에서는 RC (Rc, Arc)나 GC 같은 것이 필요하다.
22.5 ML 기반 GC 튜닝
JVM 옵션의 자동 튜닝을 ML로 하려는 시도. Facebook, Google 등이 내부에서 시도. 워크로드 특성을 학습해서 적절한 GC 옵션을 자동 선택.
23. 결론 — GC는 끝나지 않는다
이 글을 다 읽으면 다음 질문에 답할 수 있을 것이다:
- GC가 어떻게 garbage를 식별하나? (reachability)
- Mark-sweep, copying, mark-compact의 차이는?
- Generational GC가 왜 효율적인가?
- Tri-color invariant가 왜 중요한가?
- ZGC의 colored pointer는 무엇인가?
- Go GC가 왜 generational이 아닌가?
- Python GC가 어떻게 사이클을 처리하나?
- JVM GC를 어떻게 튜닝해야 하나?
GC는 65년 동안 풀고 있는 문제이고, 앞으로도 65년 더 풀 것이다. 하드웨어가 변하고, 워크로드가 변하고, 메모리 계층이 깊어지면서 GC 알고리즘도 계속 진화한다. 1959년의 mark-and-sweep과 2025년의 ZGC는 같은 핵심 아이디어를 공유하지만 — 65년의 정제와 발견이 그 사이에 있다.
이 글을 Linux 메모리 관리 글과 함께 읽으면 "내 객체가 어디 살고 언제 죽는가"라는 질문 전체에 답할 수 있게 된다. 커널은 페이지로 메모리를 나눠 프로세스에 준다. 런타임은 그 안에서 객체로 메모리를 나눠 사용자 코드에 준다. GC가 그 두 번째 단계의 자동화이다.
다음 글에서는 [Memory Allocators (jemalloc, tcmalloc, mimalloc)] 또는 [Rust ownership과 borrow checker]를 다룰 예정이다. GC는 자동화의 한 길이지만, 다른 길도 있다. 그 길의 풍경도 함께 둘러보자.
부록 A — 참고 자료
- Richard Jones, Antony Hosking, Eliot Moss, "The Garbage Collection Handbook" — GC의 사실상 표준 교과서.
- David F. Bacon et al., "A Unified Theory of Garbage Collection" — RC와 tracing GC가 같은 문제의 두 끝임을 증명.
- JEP 333: ZGC: A Scalable Low-Latency Garbage Collector — ZGC 도입 JEP.
- JEP 318: Epsilon: A No-Op Garbage Collector — Epsilon 도입.
- Shenandoah GC Wiki — Shenandoah 공식.
- Go GC: Prioritizing low latency and simplicity — Go GC 디자인.
- V8: Orinoco: young generation garbage collection — V8 GC.
- CPython GC documentation — Python GC.
- Hans Boehm의 GC 페이지 — Boehm conservative GC.
- Bartosz Milewski, "Type System Aware GC" — 다른 시각.
부록 B — 자주 묻는 질문
Q: GC가 있는 언어가 항상 느린가? A: 아니다. JVM의 처리량은 C++에 매우 가깝다 (특히 Server GC + 큰 힙). 차이는 latency tail에서 더 크게 나타난다.
Q: ZGC와 Shenandoah 중 무엇을 써야 하나? A: 둘 다 비슷한 워크로드를 다룬다. 큰 차이 없다. ZGC는 Oracle 지원, Shenandoah는 Red Hat 지원. 사용하는 JDK가 어느 쪽을 더 잘 지원하는지로 결정.
Q: Go GC가 정말 generational이 아닌가? A: 그렇다. 명시적으로 거부했다. 그러나 일부 escape analysis로 짧은 수명 객체를 스택에 두는 효과는 비슷하다.
Q: Python의 GC를 끄면 빠른가?
A: cycle collector만 끌 수 있다 (gc.disable()). RC는 못 끈다. 짧은 batch job에는 유용할 수 있다 (특히 data-heavy script).
Q: JVM에서 GC pause를 정말 0으로 만들 수 있나? A: 없다. ZGC도 sub-millisecond이지 0이 아니다. 그러나 사용자 입장에서는 보이지 않을 만큼 짧다.
Q: V8과 Node.js의 GC 차이는?
A: 같은 V8을 쓰지만, Node.js는 기본 힙 한도가 작다 (1.7GB). 큰 메모리를 쓰면 --max-old-space-size로 늘려야.
Q: Rust는 GC가 없는데 어떻게 메모리를 관리하나? A: ownership과 lifetime을 컴파일 타임에 추적. 이는 GC의 대안이지 GC의 부재가 아니다. 사이클이 필요하면 Rc/Arc를 명시적으로 사용.
Q: GC가 SSD 수명에 영향을 주나? A: 직접적으로는 아니다. 그러나 매우 큰 힙에서 swap이 일어나면 swap I/O가 SSD에 부담을 준다. 메모리 부족 상황에서 GC가 도울 수 있는 부분.
부록 C — 미니 용어집
- GC: Garbage Collection. 자동 메모리 회수.
- Root: GC의 시작점 (글로벌, 스택, 레지스터).
- Reachable: 루트에서 참조 체인으로 도달 가능한 객체.
- Mark: 도달 가능한 객체에 표시.
- Sweep: 표시되지 않은 객체를 회수.
- Compact: 살아있는 객체를 한쪽으로 모음.
- Tri-color: 흰/회/검 마킹 모델.
- Write barrier: 객체 그래프 수정 시 GC에 알리는 코드.
- Read barrier (load barrier): 객체 읽기 시 GC 검사 코드 (ZGC).
- Generational hypothesis: 대부분의 객체가 빨리 죽는다는 관찰.
- Young generation: 새 객체 영역.
- Old generation (Tenured): 살아남은 객체 영역.
- Promotion: young → old 이동.
- Card marking: cross-generation 참조 추적 기법.
- Remembered set: cross-generation 참조 컬렉션.
- STW: Stop-the-world. 모든 application 스레드 정지.
- Concurrent GC: 애플리케이션과 동시 진행하는 GC.
- G1: Garbage-First. JVM의 region-based GC.
- ZGC: Z Garbage Collector. Sub-millisecond pause.
- Shenandoah: Red Hat의 sub-ms GC.
- CMS: Concurrent Mark Sweep. 옛날 JVM concurrent GC.
- Cheney's algorithm: 우아한 BFS copying GC.
- Brooks pointer: Shenandoah의 forwarding 슬롯 (옛날).
- Colored pointers: ZGC가 포인터 비트를 GC 메타데이터로 사용.
- Pacer: GC trigger를 결정하는 컨트롤러 (Go).
- GOGC: Go의 GC 트리거 환경 변수.
- GOMEMLIMIT: Go의 soft 메모리 한도.
- LOH: Large Object Heap (.NET).
- Conservative GC: 메모리 위치의 의미를 모르는 GC (Boehm).
- Precise GC: 메모리 위치의 의미를 정확히 아는 GC.
- Finalizer: GC 시 호출되는 콜백 (보통 비권장).
- Weak reference: 카운트를 증가시키지 않는 참조.
이 글은 Linux 메모리 관리 글의 자매 작품이다. 그 글이 커널 측의 메모리 관리를 다뤘다면, 이 글은 런타임 측의 메모리 관리를 다뤘다. 두 풍경이 모이면 "내 객체가 어디 살고 언제 죽는가"의 전체 답이 그려진다. 다음 글에서는 메모리 할당자나 Rust ownership 같은 다른 길을 둘러볼 것이다.
현재 단락 (1/523)
가비지 컬렉션(GC)은 컴퓨터 과학에서 가장 오래된 문제 중 하나이다. 1959년, John McCarthy가 Lisp을 만들면서 "수동 메모리 관리는 너무 어려우니 자동화하자"고...