Split View: 메모리 관리 & 가비지 컬렉션 완전 가이드 2025: Stack/Heap, GC 알고리즘, 메모리 누수 디버깅
메모리 관리 & 가비지 컬렉션 완전 가이드 2025: Stack/Heap, GC 알고리즘, 메모리 누수 디버깅
목차
1. 들어가며: 왜 메모리 관리를 알아야 하는가
메모리 관리는 모든 프로그래밍 언어의 핵심 기반이다. GC(가비지 컬렉션)가 자동으로 메모리를 관리해 준다고 해도, 내부 동작을 이해해야 성능 문제를 진단하고 메모리 누수를 해결할 수 있다.
이 가이드에서 다루는 핵심 주제:
- 메모리 기초: 가상 메모리, 페이지, TLB
- Stack과 Heap의 구조와 차이점
- GC 알고리즘: Reference Counting, Mark-and-Sweep, Generational
- V8(JavaScript), JVM(Java), Python, Go의 GC 전략
- Rust 소유권 시스템 (GC 없는 메모리 안전)
- 메모리 누수 탐지와 디버깅
메모리 관리 전체 구조
======================
[프로세스 가상 메모리 공간]
+---------------------------+ High Address
| Kernel Space |
+---------------------------+
| Stack (grows downward) | <-- 함수 호출, 지역 변수
| ... |
| ...free... |
| ... |
| Heap (grows upward) | <-- 동적 할당 (new, malloc)
+---------------------------+
| BSS (uninitialized data) |
| Data (initialized data) |
| Text (code) |
+---------------------------+ Low Address
2. 메모리 기초
2.1 가상 메모리 (Virtual Memory)
가상 메모리 구조
================
프로세스 A의 가상 주소 공간 물리 메모리 (RAM)
+-------------------+ +-------------------+
| Page 0: 0x0000 | -------> | Frame 5 |
| Page 1: 0x1000 | -------> | Frame 12 |
| Page 2: 0x2000 | --+ | Frame 3 |
| Page 3: 0x3000 | | | ... |
+-------------------+ | +-------------------+
|
+-----> [Disk (Swap)]
Page Table (프로세스별):
+-------+-------+-------+-------+
| VPN 0 | VPN 1 | VPN 2 | VPN 3 |
| PFN:5 | PFN:12| Swap | PFN:8 |
| V=1 | V=1 | V=0 | V=1 |
+-------+-------+-------+-------+
V=1: 물리 메모리에 존재
V=0: 디스크(swap)에 존재 -> Page Fault 발생
2.2 TLB (Translation Lookaside Buffer)
TLB 동작
========
CPU가 가상 주소 접근:
1. TLB 확인 (캐시)
- Hit: 바로 물리 주소 획득 (1-2 cycle)
- Miss: Page Table 조회 (수십~수백 cycle)
2. Page Table 조회
- 해당 페이지가 메모리에 있음: PFN 반환
- 해당 페이지가 없음: Page Fault -> OS가 디스크에서 로드
TLB 구조:
+------+------+-------+
| VPN | PFN | Flags |
+------+------+-------+
| 0x5 | 0x12 | R/W |
| 0x8 | 0x3 | R |
| 0xA | 0x7 | R/W |
+------+------+-------+
TLB 적중률이 99% 이상이어야 정상적인 성능
2.3 Memory-Mapped I/O
mmap 동작 원리
==============
일반 파일 I/O:
[프로세스] -> read() -> [커널 버퍼] -> [사용자 버퍼]
데이터 복사가 2번 발생!
Memory-Mapped I/O:
[프로세스 가상 메모리] <--직접 매핑--> [파일]
장점:
- 커널/사용자 간 데이터 복사 제거
- OS 페이지 캐시 활용
- 대용량 파일 처리에 효율적
사용 예: 데이터베이스 (LMDB), 로그 파일 처리
3. Stack (스택)
3.1 Call Stack 구조
Call Stack 동작
===============
void main() {
int x = 10;
foo(x); // <-- 현재 실행 지점
}
void foo(int a) {
int y = 20;
bar(a, y);
}
void bar(int p, int q) {
int z = p + q;
}
Stack (아래에서 위로 성장):
+------------------------+
| bar의 Stack Frame | <-- Stack Pointer (SP)
| z = 30 |
| q = 20 |
| p = 10 |
| Return Address (foo) |
+------------------------+
| foo의 Stack Frame |
| y = 20 |
| a = 10 |
| Return Address (main)|
+------------------------+
| main의 Stack Frame |
| x = 10 |
| Return Address (OS) |
+------------------------+ <-- Stack Base
3.2 Stack Frame 상세
Stack Frame 구조 (x86-64)
==========================
High Address
+----------------------------+
| Argument N | (레지스터에 안 들어가는 인자)
| ... |
| Argument 7 |
+----------------------------+
| Return Address | (CALL 명령어가 push)
+----------------------------+
| Saved Base Pointer (RBP) | <-- Frame Pointer
+----------------------------+
| Local Variable 1 |
| Local Variable 2 |
| ... |
| Saved Registers |
+----------------------------+ <-- Stack Pointer (RSP)
Low Address
접근 방식:
- 지역 변수: [RBP - offset]
- 함수 인자: [RBP + offset]
3.3 Stack Overflow
Stack Overflow 발생 원인
========================
1. 무한 재귀 (가장 흔한 원인)
void infinite() {
infinite(); // Stack Frame이 계속 쌓임
}
2. 너무 큰 지역 변수
void largeLocal() {
int arr[10000000]; // 40MB on stack!
}
3. 너무 깊은 재귀
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n-1) + fibonacci(n-2);
// fib(50)은 수십억 번의 재귀 호출
}
Stack 크기 제한:
- Linux 기본: 8MB (ulimit -s)
- Windows 기본: 1MB
- Thread별 별도 Stack 할당
해결책:
- 재귀를 반복문으로 변환
- Tail Call Optimization (TCO) 활용
- 스택 크기 증가 (임시 방편)
4. Heap (힙)
4.1 동적 메모리 할당
Heap 할당 과정
==============
[프로세스 Heap 영역]
+------------------------------------------+
| [Used: 32B] [Free: 64B] [Used: 128B] |
| [Free: 256B] [Used: 16B] [Free: 512B] |
+------------------------------------------+
malloc(100) 호출:
1. Free List에서 100B 이상인 블록 검색
2. [Free: 256B] 블록 발견
3. 100B 할당 + 156B 남은 부분 Free List에 유지
결과:
+------------------------------------------+
| [Used: 32B] [Free: 64B] [Used: 128B] |
| [Used: 100B] [Free: 156B] [Used: 16B] ...|
+------------------------------------------+
4.2 메모리 단편화 (Fragmentation)
메모리 단편화 유형
==================
External Fragmentation (외부 단편화):
+---+----+---+--------+---+------+---+
|Use|Free|Use| Free |Use| Free |Use|
|32B| 8B |64B| 16B |32B| 12B |16B|
+---+----+---+--------+---+------+---+
Free 합계: 36B이지만 연속 최대 16B!
-> 20B 할당 불가 (충분한 연속 공간 없음)
Internal Fragmentation (내부 단편화):
요청: 100B, 할당: 128B (16B 단위 정렬)
-> 28B 낭비
해결 방법:
- Compaction: 사용 중인 블록을 한쪽으로 모음 (이동 비용 높음)
- Buddy System: 2의 거듭제곱 크기로 분할
- Slab Allocator: 고정 크기 객체 풀
4.3 메모리 할당기 비교
메모리 할당기 비교
==================
1. glibc malloc (ptmalloc2)
- 기본 Linux 할당기
- Thread별 arena로 lock 경합 감소
- 작은 할당: fastbin (LIFO, lock-free)
- 큰 할당: mmap 직접 사용
2. jemalloc (Facebook/Meta)
- FreeBSD 기본, Redis 사용
- Thread Cache -> Bin -> Run -> Chunk
- 크기 클래스별 미세 분류로 단편화 최소화
- 우수한 프로파일링/통계 기능
3. tcmalloc (Google)
- Thread-Caching Malloc
- Thread Local Cache: 작은 객체 lock-free 할당
- Central Free List: Thread 간 공유
- Page Heap: OS에서 큰 블록 할당
- Go 런타임의 메모리 할당기 기반
성능 비교 (상대적):
+----------+---------+---------+-----------+
| | 속도 | 메모리 | 멀티스레드 |
+----------+---------+---------+-----------+
| ptmalloc | 보통 | 보통 | 보통 |
| jemalloc | 빠름 | 우수 | 우수 |
| tcmalloc | 매우빠름| 우수 | 매우우수 |
+----------+---------+---------+-----------+
5. GC 알고리즘 기초
5.1 Reference Counting
Reference Counting 동작
=======================
let a = new Object(); // Object의 ref_count = 1
let b = a; // Object의 ref_count = 2
a = null; // Object의 ref_count = 1
b = null; // Object의 ref_count = 0 -> 즉시 해제!
장점:
- 즉시 해제: 참조 카운트가 0이 되면 바로 메모리 반환
- 예측 가능한 일시 정지 없음
- 구현이 비교적 단순
단점 (치명적):
- 순환 참조를 해결할 수 없음!
순환 참조 예시:
let a = {}; // a.ref_count = 1
let b = {}; // b.ref_count = 1
a.ref = b; // b.ref_count = 2
b.ref = a; // a.ref_count = 2
a = null; // a_obj.ref_count = 1 (b.ref가 아직 참조)
b = null; // b_obj.ref_count = 1 (a_obj.ref가 아직 참조)
// 둘 다 ref_count > 0이지만 접근 불가 -> 메모리 누수!
5.2 Mark-and-Sweep
Mark-and-Sweep 동작
====================
Phase 1: Mark (마킹)
- GC Root(스택, 전역 변수, 레지스터)에서 시작
- 도달 가능한 모든 객체에 mark 비트 설정
Phase 2: Sweep (쓸기)
- 전체 힙을 순회하며 mark 안 된 객체 해제
GC Root --> [A] --> [B] --> [C]
|
+--> [D]
[E] --> [F] (GC Root에서 도달 불가)
^ |
+-------+ (순환 참조이지만 도달 불가 -> 수거됨!)
Mark 결과: A(marked), B(marked), C(marked), D(marked)
Sweep 결과: E(해제), F(해제)
장점: 순환 참조 처리 가능
단점: Stop-the-World 일시 정지, 힙 단편화
5.3 Mark-Compact
Mark-Compact 동작
==================
Mark-and-Sweep의 단편화 문제 해결
Before:
[A][Free][B][Free][Free][C][Free][D]
After Mark-Compact:
[A][B][C][D][ Free ]
과정:
1. Mark: 도달 가능 객체 마킹 (Mark-and-Sweep과 동일)
2. Compact: 살아있는 객체를 한쪽으로 모음
3. Update References: 이동된 객체의 참조 업데이트
장점: 단편화 제거, 연속 free space 확보
단점: 객체 이동 비용, 참조 업데이트 비용
5.4 Copying GC
Copying GC 동작
================
메모리를 두 반공간(semi-space)으로 분할
From-Space (현재 사용):
[A][garbage][B][garbage][C][garbage]
To-Space (비어있음):
[ ]
Copy 과정:
1. GC Root에서 도달 가능 객체만 To-Space로 복사
2. From-Space와 To-Space 역할 교환
After:
From-Space (이전 To-Space):
[A][B][C][ Free ]
To-Space (이전 From-Space, 비워짐):
[ ]
장점: 단편화 없음, 할당이 매우 빠름 (bump pointer)
단점: 메모리 사용량 2배, 긴 수명 객체 반복 복사
5.5 Tri-color Marking
Tri-color Marking (삼색 표시)
==============================
색상 정의:
- White(흰색): 아직 방문하지 않음 (GC 대상 후보)
- Gray(회색): 방문했지만 자식을 아직 다 처리하지 않음
- Black(검정): 방문 완료, 모든 자식도 처리됨
초기 상태: Root만 Gray, 나머지 모두 White
Step 1: [Root:Gray] -> [A:White] -> [B:White]
[C:White]
Step 2: Root를 처리, 자식 A를 Gray로
[Root:Black] -> [A:Gray] -> [B:White]
[C:White]
Step 3: A를 처리, 자식 B를 Gray로
[Root:Black] -> [A:Black] -> [B:Gray]
[C:White]
Step 4: B를 처리 (자식 없음)
[Root:Black] -> [A:Black] -> [B:Black]
[C:White]
결과: C는 White -> 수거 대상
장점: 점진적(incremental) GC 가능
Concurrent GC의 기반 알고리즘
6. Generational GC (세대별 GC)
6.1 Weak Generational Hypothesis
약한 세대 가설
==============
"대부분의 객체는 생성된 직후 곧 사용되지 않게 된다"
객체 수명 분포:
|
|*
|**
|****
|********
|*****************
|*******************************************
+------------------------------------------------->
짧음 긴 수명
90% 이상의 객체가 첫 번째 GC 이전에 죽음!
-> 젊은 객체를 자주, 늙은 객체를 드물게 수거하면 효율적
6.2 JVM Generational GC 구조
JVM 힙 메모리 구조
==================
+---------------------------------------------------+
| Young Generation (전체의 1/3) |
| +--------+----------+----------+ |
| | Eden | Survivor | Survivor | |
| | | S0 | S1 | |
| | (80%) | (10%) | (10%) | |
| +--------+----------+----------+ |
+---------------------------------------------------+
| Old Generation (전체의 2/3) |
| +---------------------------------------------+ |
| | Tenured Space | |
| +---------------------------------------------+ |
+---------------------------------------------------+
| Metaspace (클래스 메타데이터, 네이티브 메모리) |
+---------------------------------------------------+
객체 생명주기:
1. Eden에서 생성
2. Minor GC: Eden 살아남은 객체 -> S0 (age=1)
3. 다음 Minor GC: Eden+S0 살아남은 객체 -> S1 (age++)
4. S0와 S1을 번갈아 사용 (Copying GC)
5. age가 임계값(기본 15) 도달 -> Old Generation으로 승격
6. Old Generation이 가득 차면 -> Major GC (Full GC)
6.3 Minor GC vs Major GC
GC 유형 비교
=============
Minor GC (Young GC):
- 대상: Young Generation (Eden + Survivor)
- 빈도: 매우 자주 (초~분 단위)
- 시간: 수 ms ~ 수십 ms
- 알고리즘: Copying GC
- STW: 짧은 일시 정지
Major GC (Old GC):
- 대상: Old Generation
- 빈도: 드물게 (분~시간 단위)
- 시간: 수백 ms ~ 수 초
- 알고리즘: Mark-Sweep 또는 Mark-Compact
- STW: 긴 일시 정지 (문제!)
Full GC:
- 대상: Young + Old + Metaspace
- 빈도: 매우 드물게
- 시간: 수 초 ~ 수십 초
- 가능하면 발생하지 않도록 튜닝해야 함!
7. V8 GC (JavaScript)
7.1 V8 Orinoco GC 아키텍처
V8 Orinoco GC 구조
===================
V8 Heap:
+------------------+---------------------------+
| Young Generation | Old Generation |
| (Semi-space) | (Mark-Sweep/Mark-Compact) |
| | |
| [From] [To] | [Old Space] |
| 1-8MB 1-8MB | [Large Object Space] |
| | [Code Space] |
| | [Map Space] |
+------------------+---------------------------+
Young Gen: Scavenger (Copying GC)
Old Gen: Mark-Compact (주요), Mark-Sweep (보조)
7.2 Scavenger (Young Generation GC)
Scavenger 동작
===============
Semi-space 모델: From-Space와 To-Space
1. 객체가 From-Space에 할당됨
2. From-Space가 가득 차면 Scavenge 시작
3. 살아있는 객체를 To-Space로 복사
4. 2번 살아남은 객체는 Old Generation으로 승격
5. From/To Space 역할 교환
할당 (Bump Pointer):
[allocated | allocated | ... | --> free space ]
^
allocation pointer
다음 할당: pointer를 크기만큼 전진 -> O(1)!
7.3 Major GC (Old Generation)
V8 Major GC 최적화 기법
========================
1. Incremental Marking (점진적 마킹)
- 전체 마킹을 작은 단위로 분할
- 애플리케이션 실행과 번갈아 수행
[App][Mark][App][Mark][App][Mark]...[Sweep]
vs. 기존:
[App]......[ Mark | Sweep ]......[App]
^--- 긴 STW pause ---^
2. Concurrent Marking (동시 마킹)
- 별도 스레드에서 마킹 수행
- 메인 스레드를 거의 블로킹하지 않음
Main Thread: [App][App][App][App][Short STW][App]
GC Thread: [ Concurrent Marking ][Done]
3. Lazy Sweeping (지연 스위핑)
- 즉시 모든 메모리를 쓸지 않음
- 새 할당이 필요할 때 점진적으로 sweep
4. Idle-time GC
- requestIdleCallback 같은 유휴 시간 활용
- 프레임 사이의 빈 시간에 GC 수행
8. JVM GC 전략
8.1 JVM GC 종류 비교
JVM GC 비교표
=============
+-------------+----------+---------+--------+------------+
| GC | 세대별 | 동시성 | STW | 적합 환경 |
+-------------+----------+---------+--------+------------+
| Serial | O | X | 긴 | 작은 힙 |
| Parallel | O | 부분 | 중간 | 처리량 우선 |
| CMS | O | O | 짧은 | 응답시간 우선|
| G1 | Region | O | 짧은 | 범용 |
| ZGC | Region | O | 매우짧 | 대용량 힙 |
| Shenandoah | Region | O | 매우짧 | 대용량 힙 |
+-------------+----------+---------+--------+------------+
STW 기준:
- Serial/Parallel: 수백 ms ~ 수 초
- CMS: 수십 ms (하지만 concurrent mode failure 시 수 초)
- G1: 수십 ms (목표 시간 설정 가능)
- ZGC: 1ms 미만 (!)
- Shenandoah: 1ms 미만
8.2 G1 GC (Garbage First)
G1 GC 구조
===========
힙을 동일 크기의 Region으로 분할 (1-32MB)
+---+---+---+---+---+---+---+---+
| E | S | O | O | H | E | E | |
+---+---+---+---+---+---+---+---+
| O | | E | S | O | O | | E |
+---+---+---+---+---+---+---+---+
E: Eden Region
S: Survivor Region
O: Old Region
H: Humongous Region (객체가 Region 크기의 50% 초과)
(공백): Free Region
동작 과정:
1. Young GC: Eden/Survivor Region만 수집
2. Concurrent Marking: 전체 힙의 liveness 분석
3. Mixed GC: Young + 가비지가 많은 Old Region 선택적 수집
-> "Garbage First" = 가비지가 가장 많은 Region부터 수집
설정:
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 // 목표 STW 시간 (기본 200ms)
-XX:G1HeapRegionSize=4m // Region 크기
8.3 ZGC
ZGC 핵심 특징
=============
목표: 어떤 힙 크기에서도 STW를 1ms 미만으로 유지
핵심 기술:
1. Colored Pointers (색상 포인터)
- 64비트 포인터의 상위 비트를 메타데이터로 활용
63 47 46 45 44 43 42 0
+------+---+---+---+---+-------+
|unused|rem|mrk1|mrk0|fin| addr |
+------+---+---+---+---+-------+
- Remapped, Marked0, Marked1, Finalizable 비트
- 주소 공간: 4TB (42비트)
2. Load Barriers (로드 배리어)
- 객체 참조를 읽을 때마다 배리어 코드 실행
- 객체가 이동되었는지 확인하고 참조 업데이트
- Write Barrier 대신 Load Barrier 사용
3. Concurrent Relocation (동시 재배치)
- 애플리케이션 실행 중 객체 이동
- Load Barrier가 이동된 객체를 즉시 업데이트
설정:
-XX:+UseZGC
-XX:SoftMaxHeapSize=4g // 소프트 힙 제한
-XX:ZCollectionInterval=5 // GC 간격(초)
8.4 Shenandoah GC
Shenandoah GC 특징
==================
ZGC와 유사한 목표: 매우 짧은 STW
핵심 차이점:
- ZGC: Colored Pointers + Load Barriers
- Shenandoah: Brooks Pointers + Read/Write Barriers
Brooks Pointer:
+-------------------+
| Brooks Pointer | --> 실제 객체 위치 (자기 자신 또는 새 위치)
+-------------------+
| Object Header |
| Object Fields |
+-------------------+
객체 이동 시:
1. 새 위치에 객체 복사
2. 원래 위치의 Brooks Pointer를 새 위치로 업데이트
3. 이후 접근 시 Brooks Pointer를 통해 새 위치로 리다이렉트
설정:
-XX:+UseShenandoahGC
-XX:ShenandoahGCHeuristics=adaptive
8.5 GC 튜닝 가이드
JVM GC 튜닝 실전
=================
1. 힙 크기 설정
-Xms4g -Xmx4g // 초기/최대 힙 크기를 동일하게
-XX:NewRatio=2 // Old:Young = 2:1
-XX:SurvivorRatio=8 // Eden:S0:S1 = 8:1:1
2. GC 선택 가이드
- 힙 크기가 작고 단순: Serial GC
- 처리량 최우선: Parallel GC
- 응답 시간이 중요한 서비스: G1 GC
- 대용량 힙, 초저지연: ZGC 또는 Shenandoah
3. GC 로그 분석
-Xlog:gc*:file=gc.log:time,uptime,level,tags
로그 확인 포인트:
- GC 빈도와 소요 시간
- Young GC와 Old GC 비율
- Allocation Rate vs Promotion Rate
- Full GC 발생 여부 (발생하면 문제!)
4. 주의사항
- Full GC가 반복되면 메모리 누수 의심
- Promotion Rate가 높으면 Young 크기 증가 고려
- Humongous 할당이 많으면 Region 크기 증가
9. Python GC
9.1 Reference Counting + Generational Cycle Collector
Python GC 이중 전략
===================
1차: Reference Counting (기본)
- 모든 객체에 참조 카운트 유지
- 카운트가 0이 되면 즉시 해제
- CPython의 핵심 메모리 관리
2차: Generational Cycle Collector (순환 참조 처리)
- 3개 세대: Generation 0, 1, 2
- 새 객체는 Gen 0에 할당
- 살아남으면 다음 세대로 승격
수집 빈도:
Gen 0: 매우 자주 (700개 할당마다)
Gen 1: 가끔 (Gen 0이 10번 수집될 때마다)
Gen 2: 드물게 (Gen 1이 10번 수집될 때마다)
# Python 순환 참조 예시
import gc
class Node:
def __init__(self, value):
self.value = value
self.next = None
# 순환 참조 생성
a = Node(1)
b = Node(2)
a.next = b # a -> b
b.next = a # b -> a (순환!)
# Reference Counting으로는 해제 불가
del a
del b
# a_obj.ref_count = 1 (b_obj.next가 참조)
# b_obj.ref_count = 1 (a_obj.next가 참조)
# Cycle Collector가 감지하여 해제
gc.collect() # 강제 순환 참조 수집
# GC 통계 확인
print(gc.get_stats())
# [{'collections': 100, 'collected': 500, 'uncollectable': 0}, ...]
10. Go GC
10.1 Concurrent Tri-color Mark-Sweep
Go GC 특징
==========
- Non-generational (세대별 구분 없음)
- Concurrent tri-color mark-sweep
- 매우 짧은 STW (보통 0.1ms 미만)
- GOGC로 GC 빈도 조절
GC 사이클:
1. Mark Setup (STW): write barrier 활성화 (~0.1ms)
2. Concurrent Mark: 애플리케이션과 동시 실행
3. Mark Termination (STW): 마킹 완료 확인 (~0.1ms)
4. Concurrent Sweep: 백그라운드에서 메모리 회수
Write Barrier:
- 마킹 중 객체 참조 변경을 추적
- Dijkstra-style write barrier 사용
- 새로 참조되는 객체를 gray로 표시
10.2 GOGC 튜닝
// Go GC 튜닝
import "runtime/debug"
// GOGC: 힙 성장 비율 설정 (기본 100)
// 100 = 라이브 힙이 2배가 되면 GC 실행
// 50 = 라이브 힙이 1.5배가 되면 GC 실행
// 200 = 라이브 힙이 3배가 되면 GC 실행
debug.SetGCPercent(100)
// GOMEMLIMIT: 메모리 상한 설정 (Go 1.19+)
// GC가 이 제한을 초과하지 않도록 더 공격적으로 수집
debug.SetMemoryLimit(4 * 1024 * 1024 * 1024) // 4GB
// GC 통계 확인
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
fmt.Printf("Alloc: %d MB\n", stats.Alloc/1024/1024)
fmt.Printf("NumGC: %d\n", stats.NumGC)
fmt.Printf("GCPauseTotal: %v\n",
time.Duration(stats.PauseTotalNs))
11. Rust 소유권 시스템
11.1 GC 없는 메모리 안전
// Rust 소유권 규칙
// 1. 각 값은 하나의 소유자(owner)만 가진다
// 2. 소유자가 스코프를 벗어나면 값이 해제된다 (RAII)
// 3. 소유권은 이동(move)하거나 빌려줄(borrow) 수 있다
fn main() {
let s1 = String::from("hello"); // s1이 소유자
let s2 = s1; // 소유권이 s2로 이동(move)
// println!("{}", s1); // 컴파일 에러! s1은 더 이상 유효하지 않음
println!("{}", s2); // OK
} // s2가 스코프를 벗어남 -> String 메모리 해제
// Stack vs Heap 할당:
fn example() {
let x = 5; // Stack (Copy trait)
let y = x; // Stack 복사 (Copy)
println!("{} {}", x, y); // 둘 다 유효
let s1 = String::from("hello"); // Heap 할당
let s2 = s1; // Move (소유권 이동)
// s1은 더 이상 사용 불가
}
11.2 빌림 (Borrowing)
// 불변 빌림 (Immutable Borrow) - 여러 개 가능
fn calculate_length(s: &String) -> usize {
s.len()
} // s는 참조만 빌렸으므로 원본에 영향 없음
// 가변 빌림 (Mutable Borrow) - 하나만 가능
fn change(s: &mut String) {
s.push_str(" world");
}
fn main() {
let mut s = String::from("hello");
// 불변 빌림: 여러 개 OK
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
// 가변 빌림: 하나만!
let r3 = &mut s;
// let r4 = &mut s; // 컴파일 에러! 동시에 2개의 가변 빌림 불가
r3.push_str("!");
// 핵심 규칙:
// - 불변 참조 N개 OR 가변 참조 1개 (동시에 불가)
// - 참조는 항상 유효해야 함 (dangling reference 방지)
}
11.3 수명 (Lifetime)
// 수명 어노테이션으로 참조의 유효 범위 명시
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
// 'a는 x와 y 중 더 짧은 수명을 따름
fn main() {
let string1 = String::from("long string");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
println!("{}", result); // OK: string2가 아직 유효
}
// println!("{}", result); // 컴파일 에러! string2가 이미 해제됨
}
// RAII (Resource Acquisition Is Initialization)
struct FileWrapper {
file: std::fs::File,
}
impl Drop for FileWrapper {
fn drop(&mut self) {
// 스코프를 벗어날 때 자동으로 호출
// 파일 핸들, 네트워크 연결 등 리소스 해제
println!("File closed automatically!");
}
}
11.4 Rust vs GC 언어 비교
Rust vs GC 언어 메모리 관리 비교
=================================
+------------------+-----------+----------+--------+
| | Rust | Java/Go | C/C++ |
+------------------+-----------+----------+--------+
| 메모리 안전 | 컴파일타임 | 런타임GC | 수동 |
| GC pause | 없음 | 있음 | 없음 |
| 메모리 오버헤드 | 낮음 | 높음 | 낮음 |
| Use-after-free | 불가 | 불가 | 가능! |
| Double free | 불가 | 불가 | 가능! |
| Memory leak | 가능* | 가능* | 가능 |
| 학습 곡선 | 매우높음 | 낮음 | 높음 |
+------------------+-----------+----------+--------+
* Rust에서도 Rc + RefCell 순환 참조,
명시적 mem::forget 등으로 메모리 누수 가능
12. 메모리 누수 탐지와 디버깅
12.1 JavaScript 메모리 누수 패턴
// 패턴 1: 제거되지 않은 이벤트 리스너
class Component {
constructor() {
// 누수! 컴포넌트 제거 시 리스너가 남음
window.addEventListener('resize', this.handleResize);
}
handleResize = () => {
// this 참조로 인해 Component가 GC 안 됨
}
// 해결: cleanup 메서드
destroy() {
window.removeEventListener('resize', this.handleResize);
}
}
// 패턴 2: 클로저가 외부 변수를 캡처
function createLeak() {
const hugeData = new Array(1000000).fill('x');
// 이 함수가 살아있는 한 hugeData도 해제 안 됨
return function() {
console.log(hugeData.length);
};
}
const leakyFn = createLeak();
// hugeData가 계속 메모리에 유지됨
// 패턴 3: 전역 캐시 무한 증가
const cache = {};
function addToCache(key, value) {
cache[key] = value; // 캐시 크기 제한 없음!
}
// 해결: WeakMap 또는 LRU Cache 사용
const weakCache = new WeakMap();
// 키 객체가 GC되면 엔트리도 자동 제거
12.2 Chrome DevTools Heap Snapshot
Chrome DevTools 메모리 분석
============================
1. Heap Snapshot 촬영
DevTools -> Memory -> Take Heap Snapshot
2. 비교 분석 (3 Snapshot Technique)
a) 앱 초기 상태에서 Snapshot 1 촬영
b) 의심되는 작업 수행
c) Snapshot 2 촬영
d) 같은 작업 반복
e) Snapshot 3 촬영
f) Snapshot 2와 3의 차이 분석
-> 반복할수록 증가하는 객체 = 누수!
3. Allocation Timeline
DevTools -> Memory -> Allocation instrumentation on timeline
- 시간에 따른 메모리 할당 패턴 시각화
- 파란 막대: 할당 후 아직 살아있음
- 회색 막대: 할당 후 GC됨
4. Retainers 확인
특정 객체를 선택하면:
- Distance: GC Root에서의 거리
- Retained Size: 이 객체가 GC되면 해제될 총 크기
- Retainers: 이 객체를 참조하고 있는 체인
12.3 Node.js 메모리 디버깅
// Node.js 메모리 모니터링
const v8 = require('v8');
const process = require('process');
// 힙 통계
function logMemory() {
const heap = v8.getHeapStatistics();
console.log({
total_heap_size: `${(heap.total_heap_size / 1024 / 1024).toFixed(2)} MB`,
used_heap_size: `${(heap.used_heap_size / 1024 / 1024).toFixed(2)} MB`,
heap_size_limit: `${(heap.heap_size_limit / 1024 / 1024).toFixed(2)} MB`,
external_memory: `${(heap.external_memory / 1024 / 1024).toFixed(2)} MB`
});
}
setInterval(logMemory, 5000);
// --inspect 플래그로 실행하여 Chrome DevTools 연결
// node --inspect app.js
// chrome://inspect 에서 연결
// Heap Snapshot을 코드에서 생성
const { writeHeapSnapshot } = require('v8');
// 특정 조건에서 스냅샷 촬영
if (process.memoryUsage().heapUsed > 500 * 1024 * 1024) {
writeHeapSnapshot();
console.log('Heap snapshot written!');
}
12.4 JVM 메모리 디버깅
# jmap: 힙 덤프 생성
jmap -dump:format=b,file=heap_dump.hprof <PID>
# jstat: GC 통계 실시간 모니터링
jstat -gcutil <PID> 1000
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 45.23 67.89 34.56 97.12 95.00 150 1.234 3 0.567 1.801
# 컬럼 의미:
# S0/S1: Survivor Space 사용률
# E: Eden Space 사용률
# O: Old Generation 사용률
# YGC/YGCT: Young GC 횟수/시간
# FGC/FGCT: Full GC 횟수/시간 (FGC가 증가하면 문제!)
VisualVM / Eclipse MAT 분석
============================
1. Heap Dump 열기 (hprof 파일)
2. Dominator Tree 확인
- 가장 많은 메모리를 보유한 객체 찾기
- Shallow Size: 객체 자체 크기
- Retained Size: 객체 + 이 객체만 참조하는 모든 객체 크기
3. Leak Suspects Report
- 자동으로 의심스러운 패턴 탐지
- "1 instance of X loaded by Y occupies Z bytes"
4. OQL (Object Query Language)
SELECT * FROM java.util.HashMap WHERE size > 10000
-> 비정상적으로 큰 컬렉션 찾기
13. 흔한 메모리 누수 패턴 종합
13.1 언어별 주요 누수 원인
메모리 누수 주요 원인
====================
JavaScript/Node.js:
1. 전역 변수에 데이터 축적
2. 제거되지 않은 이벤트 리스너
3. setInterval/setTimeout 미정리
4. 클로저가 큰 객체를 캡처
5. Detached DOM 노드 (DOM에서 제거되었지만 JS에서 참조)
Java:
1. static 컬렉션에 무한 축적
2. ThreadLocal 미정리
3. 커넥션/스트림 미닫기
4. 커스텀 ClassLoader 누수
5. 리스너/콜백 미해제
Python:
1. 순환 참조 (gc.collect()로 해결 가능)
2. __del__ 메서드가 있는 순환 참조 (gc가 수거 못 할 수 있음)
3. C 확장 모듈의 메모리 관리 오류
4. 전역 딕셔너리 무한 성장
Go:
1. goroutine 누수 (종료되지 않는 goroutine)
2. time.After 반복 사용 (timer 누적)
3. 슬라이스의 기저 배열 참조 유지
4. sync.Pool 오용
13.2 Detached DOM 노드 (JavaScript)
// Detached DOM 누수 예시
let detachedNodes = [];
function createLeak() {
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.innerHTML = 'content '.repeat(1000);
document.body.appendChild(div);
// DOM에서 제거하지만 JS 배열에서 참조 유지!
document.body.removeChild(div);
detachedNodes.push(div); // 누수!
}
}
// 해결: 참조도 함께 제거
function cleanup() {
detachedNodes = [];
// 또는 WeakRef 사용
}
13.3 Goroutine 누수 (Go)
// goroutine 누수 예시
func leakyFunction() {
ch := make(chan int)
go func() {
val := <-ch // 이 채널에 아무도 보내지 않으면 영원히 대기!
fmt.Println(val)
}()
// ch에 값을 보내지 않고 함수 종료
// goroutine은 영원히 살아있음 -> 누수!
}
// 해결: context를 사용한 취소
func properFunction(ctx context.Context) {
ch := make(chan int)
go func() {
select {
case val := <-ch:
fmt.Println(val)
case <-ctx.Done():
return // context 취소 시 goroutine 종료
}
}()
}
14. 프로파일링 도구 비교
메모리 프로파일링 도구 비교
============================
JavaScript/Node.js:
+----------------------------+--------------------+
| 도구 | 용도 |
+----------------------------+--------------------+
| Chrome DevTools Memory | 브라우저 힙 분석 |
| node --inspect | Node.js 원격 디버깅|
| clinic.js | 성능/메모리 분석 |
| heapdump 패키지 | 프로그래밍 방식 |
+----------------------------+--------------------+
Java:
+----------------------------+--------------------+
| VisualVM | 범용 프로파일링 |
| Eclipse MAT | 힙 덤프 분석 |
| JFR (Flight Recorder) | 저오버헤드 프로파일|
| async-profiler | CPU+힙 프로파일 |
| jmap/jhat | 커맨드라인 분석 |
+----------------------------+--------------------+
Go:
+----------------------------+--------------------+
| pprof | 내장 프로파일러 |
| runtime.ReadMemStats | 런타임 통계 |
| go tool trace | 실행 추적 |
+----------------------------+--------------------+
Rust:
+----------------------------+--------------------+
| Valgrind (Massif) | 힙 프로파일링 |
| heaptrack | 힙 추적 |
| DHAT | 동적 힙 분석 |
+----------------------------+--------------------+
범용:
+----------------------------+--------------------+
| Valgrind (Memcheck) | 메모리 오류 탐지 |
| AddressSanitizer (ASan) | 빠른 메모리 오류 |
| LeakSanitizer (LSan) | 누수 전용 탐지 |
+----------------------------+--------------------+
15. 퀴즈
메모리 관리와 가비지 컬렉션에 대한 이해를 점검해 보자.
Q1. Mark-and-Sweep GC가 Reference Counting보다 순환 참조 처리에 유리한 이유는?
A: Reference Counting은 각 객체의 참조 수를 추적하므로, A와 B가 서로를 참조하면 둘 다 참조 카운트가 1 이상으로 유지되어 영원히 해제되지 않는다. 반면 Mark-and-Sweep은 GC Root(스택, 전역 변수)에서 도달 가능한 객체만 마킹하므로, 순환 참조가 있더라도 GC Root에서 도달할 수 없으면 마킹되지 않아 수거된다. 즉 "도달 가능성(reachability)" 기반이므로 순환 참조와 무관하게 정확히 사용 중인 객체만 식별한다.
Q2. JVM에서 Full GC가 자주 발생하는 원인과 해결 방법은?
A: Full GC가 자주 발생하는 주요 원인은 다음과 같다. (1) Old Generation이 너무 작아서 자주 가득 참. 힙 크기(-Xmx)를 늘리거나 Old 비율을 조정한다. (2) 메모리 누수로 Old Gen에 객체가 계속 쌓임. 힙 덤프를 분석하여 누수 원인을 찾아야 한다. (3) Young Generation이 너무 작아서 객체가 조기 승격됨. -XX:NewRatio나 -XX:NewSize를 조정한다. (4) Humongous 할당이 잦아 Old Gen을 빠르게 채움(G1). Region 크기를 늘린다. jstat으로 GC 패턴을 모니터링하고, 힙 덤프로 누수를 분석하는 것이 핵심이다.
Q3. Rust의 소유권 시스템에서 동시에 가변 빌림을 2개 허용하지 않는 이유는?
A: 데이터 레이스(data race)를 컴파일 타임에 방지하기 위해서다. 데이터 레이스는 두 개 이상의 포인터가 동시에 같은 데이터에 접근하고, 그 중 하나 이상이 쓰기를 수행하며, 접근이 동기화되지 않을 때 발생한다. Rust는 "불변 참조 여러 개 OR 가변 참조 하나"라는 규칙으로 이 세 조건 중 하나를 구조적으로 제거한다. 가변 빌림이 하나뿐이면 다른 코드가 동시에 같은 데이터를 읽거나 쓸 수 없으므로, GC 없이도 메모리 안전성과 스레드 안전성을 동시에 보장한다.
Q4. V8의 Incremental Marking이 기존 Mark-and-Sweep보다 나은 점은?
A: 기존 Mark-and-Sweep은 마킹 단계 전체를 한 번에 수행하므로, 힙이 크면 수백 ms의 Stop-the-World(STW) 일시 정지가 발생한다. 이는 60fps 렌더링(프레임당 16.6ms)에서 심각한 버벅임을 유발한다. Incremental Marking은 마킹 작업을 작은 단위로 분할하여 애플리케이션 실행과 번갈아 수행한다. 각 마킹 단위는 짧은 시간만 차지하므로 사용자가 인지하는 일시 정지가 크게 줄어든다. Tri-color marking을 기반으로 중단/재개가 가능하며, write barrier를 통해 마킹 중 변경된 참조도 정확히 추적한다.
Q5. Chrome DevTools의 3-Snapshot Technique로 메모리 누수를 탐지하는 과정을 설명하시오.
A: (1) 앱의 초기 상태에서 첫 번째 힙 스냅샷을 촬영한다. (2) 누수가 의심되는 작업(예: 페이지 이동 후 돌아오기)을 수행한다. (3) 두 번째 힙 스냅샷을 촬영한다. (4) 같은 작업을 다시 반복한다. (5) 세 번째 힙 스냅샷을 촬영한다. (6) 스냅샷 2와 3을 비교하여, 작업을 반복할 때마다 지속적으로 증가하는 객체를 찾는다. 해당 객체가 누수의 원인이다. Retainers 패널에서 어떤 참조 체인이 해당 객체를 유지하고 있는지 확인하면 누수의 근본 원인을 파악할 수 있다.
16. 참고 자료
- Computer Systems: A Programmer's Perspective - Bryant & O'Hallaron (3rd Edition)
- The Garbage Collection Handbook - Jones, Hosking, Moss (2nd Edition, 2023)
- V8 Blog: Orinoco - https://v8.dev/blog/trash-talk
- JVM GC Tuning Guide - https://docs.oracle.com/en/java/javase/21/gctuning/
- The Rust Programming Language - https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html
- Go GC Guide - https://tip.golang.org/doc/gc-guide
- Chrome DevTools Memory - https://developer.chrome.com/docs/devtools/memory-problems/
- ZGC Documentation - https://wiki.openjdk.org/display/zgc
- jemalloc - https://jemalloc.net/
- Python GC Documentation - https://docs.python.org/3/library/gc.html
- Understanding V8 Memory Structure - https://deepu.tech/memory-management-in-v8/
- Shenandoah GC - https://wiki.openjdk.org/display/shenandoah
- Valgrind Quick Start - https://valgrind.org/docs/manual/quick-start.html
Memory Management & Garbage Collection Complete Guide 2025: Stack/Heap, GC Algorithms, Memory Leak Debugging
Table of Contents
1. Introduction: Why Understand Memory Management
Memory management is the fundamental foundation of every programming language. Even though GC (garbage collection) automatically manages memory, understanding its internals is essential for diagnosing performance issues and resolving memory leaks.
Key topics covered in this guide:
- Memory fundamentals: Virtual memory, pages, TLB
- Stack and Heap structure and differences
- GC algorithms: Reference Counting, Mark-and-Sweep, Generational
- GC strategies in V8 (JavaScript), JVM (Java), Python, Go
- Rust ownership system (memory safety without GC)
- Memory leak detection and debugging
Memory Management Overall Structure
=====================================
[Process Virtual Memory Space]
+---------------------------+ High Address
| Kernel Space |
+---------------------------+
| Stack (grows downward) | <-- function calls, local variables
| ... |
| ...free... |
| ... |
| Heap (grows upward) | <-- dynamic allocation (new, malloc)
+---------------------------+
| BSS (uninitialized data) |
| Data (initialized data) |
| Text (code) |
+---------------------------+ Low Address
2. Memory Fundamentals
2.1 Virtual Memory
Virtual Memory Structure
=========================
Process A Virtual Address Space Physical Memory (RAM)
+-------------------+ +-------------------+
| Page 0: 0x0000 | ----------> | Frame 5 |
| Page 1: 0x1000 | ----------> | Frame 12 |
| Page 2: 0x2000 | --+ | Frame 3 |
| Page 3: 0x3000 | | | ... |
+-------------------+ | +-------------------+
|
+-----> [Disk (Swap)]
Page Table (per process):
+-------+-------+-------+-------+
| VPN 0 | VPN 1 | VPN 2 | VPN 3 |
| PFN:5 | PFN:12| Swap | PFN:8 |
| V=1 | V=1 | V=0 | V=1 |
+-------+-------+-------+-------+
V=1: Present in physical memory
V=0: On disk (swap) -> Page Fault occurs
2.2 TLB (Translation Lookaside Buffer)
TLB Operation
==============
CPU accesses virtual address:
1. Check TLB (cache)
- Hit: Get physical address immediately (1-2 cycles)
- Miss: Walk Page Table (tens to hundreds of cycles)
2. Page Table lookup
- Page in memory: Return PFN
- Page not in memory: Page Fault -> OS loads from disk
TLB Structure:
+------+------+-------+
| VPN | PFN | Flags |
+------+------+-------+
| 0x5 | 0x12 | R/W |
| 0x8 | 0x3 | R |
| 0xA | 0x7 | R/W |
+------+------+-------+
TLB hit rate must be 99%+ for normal performance
2.3 Memory-Mapped I/O
mmap Operating Principle
=========================
Regular file I/O:
[Process] -> read() -> [Kernel Buffer] -> [User Buffer]
Data copied twice!
Memory-Mapped I/O:
[Process Virtual Memory] <--direct mapping--> [File]
Advantages:
- Eliminates kernel/user data copying
- Leverages OS page cache
- Efficient for large file processing
Usage examples: databases (LMDB), log file processing
3. Stack
3.1 Call Stack Structure
Call Stack Operation
=====================
void main() {
int x = 10;
foo(x); // <-- current execution point
}
void foo(int a) {
int y = 20;
bar(a, y);
}
void bar(int p, int q) {
int z = p + q;
}
Stack (grows bottom to top):
+------------------------+
| bar's Stack Frame | <-- Stack Pointer (SP)
| z = 30 |
| q = 20 |
| p = 10 |
| Return Address (foo) |
+------------------------+
| foo's Stack Frame |
| y = 20 |
| a = 10 |
| Return Address (main)|
+------------------------+
| main's Stack Frame |
| x = 10 |
| Return Address (OS) |
+------------------------+ <-- Stack Base
3.2 Stack Frame Details
Stack Frame Structure (x86-64)
================================
High Address
+----------------------------+
| Argument N | (args that don't fit in registers)
| ... |
| Argument 7 |
+----------------------------+
| Return Address | (pushed by CALL instruction)
+----------------------------+
| Saved Base Pointer (RBP) | <-- Frame Pointer
+----------------------------+
| Local Variable 1 |
| Local Variable 2 |
| ... |
| Saved Registers |
+----------------------------+ <-- Stack Pointer (RSP)
Low Address
Access patterns:
- Local variables: [RBP - offset]
- Function arguments: [RBP + offset]
3.3 Stack Overflow
Stack Overflow Causes
======================
1. Infinite recursion (most common)
void infinite() {
infinite(); // Stack frames keep accumulating
}
2. Excessively large local variables
void largeLocal() {
int arr[10000000]; // 40MB on stack!
}
3. Too deep recursion
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n-1) + fibonacci(n-2);
// fib(50) results in billions of recursive calls
}
Stack size limits:
- Linux default: 8MB (ulimit -s)
- Windows default: 1MB
- Separate stack allocated per thread
Solutions:
- Convert recursion to iteration
- Use Tail Call Optimization (TCO)
- Increase stack size (temporary measure)
4. Heap
4.1 Dynamic Memory Allocation
Heap Allocation Process
========================
[Process Heap Region]
+------------------------------------------+
| [Used: 32B] [Free: 64B] [Used: 128B] |
| [Free: 256B] [Used: 16B] [Free: 512B] |
+------------------------------------------+
malloc(100) called:
1. Search Free List for block of 100B or more
2. Found [Free: 256B]
3. Allocate 100B + keep remaining 156B in Free List
Result:
+------------------------------------------+
| [Used: 32B] [Free: 64B] [Used: 128B] |
| [Used: 100B] [Free: 156B] [Used: 16B] ...|
+------------------------------------------+
4.2 Memory Fragmentation
Memory Fragmentation Types
============================
External Fragmentation:
+---+----+---+--------+---+------+---+
|Use|Free|Use| Free |Use| Free |Use|
|32B| 8B |64B| 16B |32B| 12B |16B|
+---+----+---+--------+---+------+---+
Free total: 36B but max contiguous is only 16B!
-> Cannot allocate 20B (not enough contiguous space)
Internal Fragmentation:
Requested: 100B, Allocated: 128B (16B alignment)
-> 28B wasted
Solutions:
- Compaction: Move used blocks to one side (high movement cost)
- Buddy System: Split into power-of-2 sizes
- Slab Allocator: Fixed-size object pools
4.3 Memory Allocator Comparison
Memory Allocator Comparison
=============================
1. glibc malloc (ptmalloc2)
- Default Linux allocator
- Per-thread arenas to reduce lock contention
- Small allocations: fastbin (LIFO, lock-free)
- Large allocations: direct mmap
2. jemalloc (Facebook/Meta)
- FreeBSD default, used by Redis
- Thread Cache -> Bin -> Run -> Chunk
- Fine-grained size classes minimize fragmentation
- Excellent profiling/statistics capabilities
3. tcmalloc (Google)
- Thread-Caching Malloc
- Thread Local Cache: lock-free allocation for small objects
- Central Free List: shared between threads
- Page Heap: large block allocation from OS
- Foundation for Go runtime memory allocator
Performance comparison (relative):
+----------+---------+---------+---------------+
| | Speed | Memory | Multi-thread |
+----------+---------+---------+---------------+
| ptmalloc | Average | Average | Average |
| jemalloc | Fast | Good | Good |
| tcmalloc | V.Fast | Good | Excellent |
+----------+---------+---------+---------------+
5. GC Algorithm Fundamentals
5.1 Reference Counting
Reference Counting Operation
==============================
let a = new Object(); // Object ref_count = 1
let b = a; // Object ref_count = 2
a = null; // Object ref_count = 1
b = null; // Object ref_count = 0 -> freed immediately!
Pros:
- Immediate deallocation: memory returned as soon as count hits 0
- No unpredictable pauses
- Relatively simple implementation
Cons (critical):
- Cannot handle circular references!
Circular reference example:
let a = {}; // a.ref_count = 1
let b = {}; // b.ref_count = 1
a.ref = b; // b.ref_count = 2
b.ref = a; // a.ref_count = 2
a = null; // a_obj.ref_count = 1 (b.ref still references)
b = null; // b_obj.ref_count = 1 (a_obj.ref still references)
// Both ref_count > 0 but unreachable -> memory leak!
5.2 Mark-and-Sweep
Mark-and-Sweep Operation
==========================
Phase 1: Mark
- Start from GC Roots (stack, global variables, registers)
- Set mark bit on all reachable objects
Phase 2: Sweep
- Traverse entire heap and free unmarked objects
GC Root --> [A] --> [B] --> [C]
|
+--> [D]
[E] --> [F] (unreachable from GC Root)
^ |
+-------+ (circular reference but unreachable -> collected!)
Mark result: A(marked), B(marked), C(marked), D(marked)
Sweep result: E(freed), F(freed)
Pros: Handles circular references
Cons: Stop-the-World pause, heap fragmentation
5.3 Mark-Compact
Mark-Compact Operation
========================
Solves the fragmentation problem of Mark-and-Sweep
Before:
[A][Free][B][Free][Free][C][Free][D]
After Mark-Compact:
[A][B][C][D][ Free ]
Process:
1. Mark: Mark reachable objects (same as Mark-and-Sweep)
2. Compact: Move live objects to one end
3. Update References: Update references to moved objects
Pros: Eliminates fragmentation, contiguous free space
Cons: Object movement cost, reference update cost
5.4 Copying GC
Copying GC Operation
=====================
Divides memory into two semi-spaces
From-Space (currently in use):
[A][garbage][B][garbage][C][garbage]
To-Space (empty):
[ ]
Copy process:
1. Copy only reachable objects from GC Root to To-Space
2. Swap roles of From-Space and To-Space
After:
From-Space (former To-Space):
[A][B][C][ Free ]
To-Space (former From-Space, cleared):
[ ]
Pros: No fragmentation, very fast allocation (bump pointer)
Cons: 2x memory usage, long-lived objects copied repeatedly
5.5 Tri-color Marking
Tri-color Marking
==================
Color definitions:
- White: Not yet visited (candidate for GC)
- Gray: Visited but children not fully processed
- Black: Fully visited, all children processed
Initial state: Only Root is Gray, everything else is White
Step 1: [Root:Gray] -> [A:White] -> [B:White]
[C:White]
Step 2: Process Root, mark child A as Gray
[Root:Black] -> [A:Gray] -> [B:White]
[C:White]
Step 3: Process A, mark child B as Gray
[Root:Black] -> [A:Black] -> [B:Gray]
[C:White]
Step 4: Process B (no children)
[Root:Black] -> [A:Black] -> [B:Black]
[C:White]
Result: C remains White -> collection target
Pros: Enables incremental GC
Foundation algorithm for concurrent GC
6. Generational GC
6.1 Weak Generational Hypothesis
Weak Generational Hypothesis
==============================
"Most objects die shortly after creation"
Object lifetime distribution:
|
|*
|**
|****
|********
|*****************
|*******************************************
+------------------------------------------------->
short long lifetime
90%+ of objects die before the first GC!
-> Collecting young objects frequently and old objects rarely is efficient
6.2 JVM Generational GC Structure
JVM Heap Memory Structure
===========================
+---------------------------------------------------+
| Young Generation (1/3 of total) |
| +--------+----------+----------+ |
| | Eden | Survivor | Survivor | |
| | | S0 | S1 | |
| | (80%) | (10%) | (10%) | |
| +--------+----------+----------+ |
+---------------------------------------------------+
| Old Generation (2/3 of total) |
| +---------------------------------------------+ |
| | Tenured Space | |
| +---------------------------------------------+ |
+---------------------------------------------------+
| Metaspace (class metadata, native memory) |
+---------------------------------------------------+
Object lifecycle:
1. Created in Eden
2. Minor GC: surviving objects from Eden -> S0 (age=1)
3. Next Minor GC: surviving from Eden+S0 -> S1 (age++)
4. Alternates between S0 and S1 (Copying GC)
5. Age reaches threshold (default 15) -> promoted to Old Generation
6. Old Generation fills up -> Major GC (Full GC)
6.3 Minor GC vs Major GC
GC Type Comparison
===================
Minor GC (Young GC):
- Target: Young Generation (Eden + Survivor)
- Frequency: Very often (seconds to minutes)
- Duration: Milliseconds to tens of ms
- Algorithm: Copying GC
- STW: Short pause
Major GC (Old GC):
- Target: Old Generation
- Frequency: Infrequent (minutes to hours)
- Duration: Hundreds of ms to seconds
- Algorithm: Mark-Sweep or Mark-Compact
- STW: Long pause (problem!)
Full GC:
- Target: Young + Old + Metaspace
- Frequency: Very rare
- Duration: Seconds to tens of seconds
- Should be tuned to prevent occurrence!
7. V8 GC (JavaScript)
7.1 V8 Orinoco GC Architecture
V8 Orinoco GC Structure
=========================
V8 Heap:
+------------------+---------------------------+
| Young Generation | Old Generation |
| (Semi-space) | (Mark-Sweep/Mark-Compact) |
| | |
| [From] [To] | [Old Space] |
| 1-8MB 1-8MB | [Large Object Space] |
| | [Code Space] |
| | [Map Space] |
+------------------+---------------------------+
Young Gen: Scavenger (Copying GC)
Old Gen: Mark-Compact (primary), Mark-Sweep (secondary)
7.2 Scavenger (Young Generation GC)
Scavenger Operation
====================
Semi-space model: From-Space and To-Space
1. Objects allocated in From-Space
2. When From-Space fills, Scavenge begins
3. Live objects copied to To-Space
4. Objects that survived twice promoted to Old Generation
5. From/To Space roles swapped
Allocation (Bump Pointer):
[allocated | allocated | ... | --> free space ]
^
allocation pointer
Next allocation: advance pointer by size -> O(1)!
7.3 Major GC (Old Generation)
V8 Major GC Optimization Techniques
=====================================
1. Incremental Marking
- Splits full marking into small increments
- Interleaved with application execution
[App][Mark][App][Mark][App][Mark]...[Sweep]
vs. Traditional:
[App]......[ Mark | Sweep ]......[App]
^--- long STW pause ---^
2. Concurrent Marking
- Marking performed on separate thread
- Barely blocks the main thread
Main Thread: [App][App][App][App][Short STW][App]
GC Thread: [ Concurrent Marking ][Done]
3. Lazy Sweeping
- Does not immediately sweep all memory
- Sweeps incrementally as new allocations are needed
4. Idle-time GC
- Utilizes idle time like requestIdleCallback
- Performs GC during gaps between frames
8. JVM GC Strategies
8.1 JVM GC Type Comparison
JVM GC Comparison Table
=========================
+-------------+------------+-----------+--------+---------------+
| GC | Generation | Concurrent| STW | Best For |
+-------------+------------+-----------+--------+---------------+
| Serial | Yes | No | Long | Small heaps |
| Parallel | Yes | Partial | Medium | Throughput |
| CMS | Yes | Yes | Short | Latency |
| G1 | Region | Yes | Short | General |
| ZGC | Region | Yes | V.Short| Large heaps |
| Shenandoah | Region | Yes | V.Short| Large heaps |
+-------------+------------+-----------+--------+---------------+
STW benchmarks:
- Serial/Parallel: hundreds of ms to seconds
- CMS: tens of ms (but seconds on concurrent mode failure)
- G1: tens of ms (pause target configurable)
- ZGC: sub-millisecond (!)
- Shenandoah: sub-millisecond
8.2 G1 GC (Garbage First)
G1 GC Structure
================
Heap divided into equal-sized Regions (1-32MB)
+---+---+---+---+---+---+---+---+
| E | S | O | O | H | E | E | |
+---+---+---+---+---+---+---+---+
| O | | E | S | O | O | | E |
+---+---+---+---+---+---+---+---+
E: Eden Region
S: Survivor Region
O: Old Region
H: Humongous Region (object exceeds 50% of Region size)
(blank): Free Region
Operation:
1. Young GC: Collects only Eden/Survivor Regions
2. Concurrent Marking: Analyzes liveness across entire heap
3. Mixed GC: Selectively collects Young + garbage-heavy Old Regions
-> "Garbage First" = collects Regions with most garbage first
Configuration:
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 // Target STW time (default 200ms)
-XX:G1HeapRegionSize=4m // Region size
8.3 ZGC
ZGC Key Features
=================
Goal: Keep STW under 1ms regardless of heap size
Core technologies:
1. Colored Pointers
- Uses upper bits of 64-bit pointer as metadata
63 47 46 45 44 43 42 0
+------+---+---+---+---+-------+
|unused|rem|mrk1|mrk0|fin| addr |
+------+---+---+---+---+-------+
- Remapped, Marked0, Marked1, Finalizable bits
- Address space: 4TB (42 bits)
2. Load Barriers
- Barrier code runs each time an object reference is read
- Checks if object has been relocated, updates reference
- Uses Load Barrier instead of Write Barrier
3. Concurrent Relocation
- Relocates objects while application is running
- Load Barrier immediately updates relocated objects
Configuration:
-XX:+UseZGC
-XX:SoftMaxHeapSize=4g // Soft heap limit
-XX:ZCollectionInterval=5 // GC interval (seconds)
8.4 Shenandoah GC
Shenandoah GC Features
========================
Similar goal to ZGC: very short STW
Key differences:
- ZGC: Colored Pointers + Load Barriers
- Shenandoah: Brooks Pointers + Read/Write Barriers
Brooks Pointer:
+-------------------+
| Brooks Pointer | --> actual object location (self or new location)
+-------------------+
| Object Header |
| Object Fields |
+-------------------+
During object relocation:
1. Copy object to new location
2. Update Brooks Pointer at original location to point to new location
3. Subsequent accesses redirect through Brooks Pointer
Configuration:
-XX:+UseShenandoahGC
-XX:ShenandoahGCHeuristics=adaptive
8.5 GC Tuning Guide
JVM GC Tuning in Practice
===========================
1. Heap size configuration
-Xms4g -Xmx4g // Set initial/max heap size equal
-XX:NewRatio=2 // Old:Young = 2:1
-XX:SurvivorRatio=8 // Eden:S0:S1 = 8:1:1
2. GC selection guide
- Small heap, simple: Serial GC
- Throughput priority: Parallel GC
- Latency-sensitive services: G1 GC
- Large heaps, ultra-low latency: ZGC or Shenandoah
3. GC log analysis
-Xlog:gc*:file=gc.log:time,uptime,level,tags
Key checkpoints:
- GC frequency and duration
- Young GC to Old GC ratio
- Allocation Rate vs Promotion Rate
- Full GC occurrence (problem if occurring!)
4. Cautions
- Repeated Full GC suggests memory leak
- High Promotion Rate: consider increasing Young size
- Frequent Humongous allocations: increase Region size
9. Python GC
9.1 Reference Counting + Generational Cycle Collector
Python GC Dual Strategy
=========================
Primary: Reference Counting (default)
- Maintains reference count on every object
- Freed immediately when count reaches 0
- Core memory management of CPython
Secondary: Generational Cycle Collector (handles circular refs)
- 3 generations: Generation 0, 1, 2
- New objects allocated in Gen 0
- Survivors promoted to next generation
Collection frequency:
Gen 0: Very frequent (every 700 allocations)
Gen 1: Occasionally (every 10 Gen 0 collections)
Gen 2: Rarely (every 10 Gen 1 collections)
# Python circular reference example
import gc
class Node:
def __init__(self, value):
self.value = value
self.next = None
# Create circular reference
a = Node(1)
b = Node(2)
a.next = b # a -> b
b.next = a # b -> a (circular!)
# Cannot be freed by Reference Counting
del a
del b
# a_obj.ref_count = 1 (b_obj.next references it)
# b_obj.ref_count = 1 (a_obj.next references it)
# Cycle Collector detects and frees them
gc.collect() # Force cycle collection
# Check GC statistics
print(gc.get_stats())
# [{'collections': 100, 'collected': 500, 'uncollectable': 0}, ...]
10. Go GC
10.1 Concurrent Tri-color Mark-Sweep
Go GC Features
===============
- Non-generational (no generation distinction)
- Concurrent tri-color mark-sweep
- Very short STW (typically under 0.1ms)
- GC frequency controlled by GOGC
GC cycle:
1. Mark Setup (STW): Enable write barrier (~0.1ms)
2. Concurrent Mark: Runs concurrently with application
3. Mark Termination (STW): Confirm marking complete (~0.1ms)
4. Concurrent Sweep: Reclaim memory in background
Write Barrier:
- Tracks object reference changes during marking
- Uses Dijkstra-style write barrier
- Marks newly referenced objects as gray
10.2 GOGC Tuning
// Go GC tuning
import "runtime/debug"
// GOGC: Set heap growth ratio (default 100)
// 100 = GC runs when live heap doubles
// 50 = GC runs when live heap grows by 50%
// 200 = GC runs when live heap triples
debug.SetGCPercent(100)
// GOMEMLIMIT: Set memory limit (Go 1.19+)
// GC collects more aggressively to stay under this limit
debug.SetMemoryLimit(4 * 1024 * 1024 * 1024) // 4GB
// Check GC statistics
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
fmt.Printf("Alloc: %d MB\n", stats.Alloc/1024/1024)
fmt.Printf("NumGC: %d\n", stats.NumGC)
fmt.Printf("GCPauseTotal: %v\n",
time.Duration(stats.PauseTotalNs))
11. Rust Ownership System
11.1 Memory Safety Without GC
// Rust ownership rules
// 1. Each value has exactly one owner
// 2. When the owner goes out of scope, the value is dropped (RAII)
// 3. Ownership can be moved or borrowed
fn main() {
let s1 = String::from("hello"); // s1 is the owner
let s2 = s1; // Ownership moves to s2
// println!("{}", s1); // Compile error! s1 is no longer valid
println!("{}", s2); // OK
} // s2 goes out of scope -> String memory freed
// Stack vs Heap allocation:
fn example() {
let x = 5; // Stack (Copy trait)
let y = x; // Stack copy (Copy)
println!("{} {}", x, y); // Both valid
let s1 = String::from("hello"); // Heap allocation
let s2 = s1; // Move (ownership transfer)
// s1 can no longer be used
}
11.2 Borrowing
// Immutable Borrow - multiple allowed
fn calculate_length(s: &String) -> usize {
s.len()
} // s only borrowed a reference, original unaffected
// Mutable Borrow - only one allowed
fn change(s: &mut String) {
s.push_str(" world");
}
fn main() {
let mut s = String::from("hello");
// Immutable borrows: multiple OK
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
// Mutable borrow: only one!
let r3 = &mut s;
// let r4 = &mut s; // Compile error! Cannot have 2 mutable borrows
r3.push_str("!");
// Core rules:
// - N immutable references OR 1 mutable reference (not both)
// - References must always be valid (prevents dangling references)
}
11.3 Lifetimes
// Lifetime annotations explicitly specify reference validity scope
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
// 'a follows the shorter lifetime of x and y
fn main() {
let string1 = String::from("long string");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
println!("{}", result); // OK: string2 still valid
}
// println!("{}", result); // Compile error! string2 already dropped
}
// RAII (Resource Acquisition Is Initialization)
struct FileWrapper {
file: std::fs::File,
}
impl Drop for FileWrapper {
fn drop(&mut self) {
// Automatically called when going out of scope
// Release file handles, network connections, etc.
println!("File closed automatically!");
}
}
11.4 Rust vs GC Languages Comparison
Rust vs GC Language Memory Management Comparison
==================================================
+------------------+-----------+----------+--------+
| | Rust | Java/Go | C/C++ |
+------------------+-----------+----------+--------+
| Memory safety | Compile | Runtime | Manual |
| GC pause | None | Yes | None |
| Memory overhead | Low | High | Low |
| Use-after-free | Impossible| Impossible| Possible!|
| Double free | Impossible| Impossible| Possible!|
| Memory leak | Possible* | Possible*| Possible |
| Learning curve | Very high | Low | High |
+------------------+-----------+----------+--------+
* Even in Rust, memory leaks possible via Rc + RefCell
circular refs, explicit mem::forget, etc.
12. Memory Leak Detection and Debugging
12.1 JavaScript Memory Leak Patterns
// Pattern 1: Unremoved event listeners
class Component {
constructor() {
// Leak! Listener remains when component is removed
window.addEventListener('resize', this.handleResize);
}
handleResize = () => {
// this reference prevents Component from being GC'd
}
// Solution: cleanup method
destroy() {
window.removeEventListener('resize', this.handleResize);
}
}
// Pattern 2: Closures capturing outer variables
function createLeak() {
const hugeData = new Array(1000000).fill('x');
// As long as this function lives, hugeData cannot be freed
return function() {
console.log(hugeData.length);
};
}
const leakyFn = createLeak();
// hugeData stays in memory
// Pattern 3: Unbounded global cache growth
const cache = {};
function addToCache(key, value) {
cache[key] = value; // No cache size limit!
}
// Solution: Use WeakMap or LRU Cache
const weakCache = new WeakMap();
// Entry auto-removed when key object is GC'd
12.2 Chrome DevTools Heap Snapshot
Chrome DevTools Memory Analysis
================================
1. Take Heap Snapshot
DevTools -> Memory -> Take Heap Snapshot
2. Comparison Analysis (3 Snapshot Technique)
a) Take Snapshot 1 at app initial state
b) Perform suspected leaky operation
c) Take Snapshot 2
d) Repeat same operation
e) Take Snapshot 3
f) Analyze difference between Snapshot 2 and 3
-> Objects that grow with each repetition = leak!
3. Allocation Timeline
DevTools -> Memory -> Allocation instrumentation on timeline
- Visualizes memory allocation patterns over time
- Blue bars: allocated and still alive
- Gray bars: allocated then GC'd
4. Check Retainers
When selecting a specific object:
- Distance: Distance from GC Root
- Retained Size: Total size freed if this object is GC'd
- Retainers: Reference chain keeping this object alive
12.3 Node.js Memory Debugging
// Node.js memory monitoring
const v8 = require('v8');
const process = require('process');
// Heap statistics
function logMemory() {
const heap = v8.getHeapStatistics();
console.log({
total_heap_size: `${(heap.total_heap_size / 1024 / 1024).toFixed(2)} MB`,
used_heap_size: `${(heap.used_heap_size / 1024 / 1024).toFixed(2)} MB`,
heap_size_limit: `${(heap.heap_size_limit / 1024 / 1024).toFixed(2)} MB`,
external_memory: `${(heap.external_memory / 1024 / 1024).toFixed(2)} MB`
});
}
setInterval(logMemory, 5000);
// Run with --inspect flag to connect Chrome DevTools
// node --inspect app.js
// Connect at chrome://inspect
// Generate Heap Snapshot from code
const { writeHeapSnapshot } = require('v8');
// Take snapshot under certain conditions
if (process.memoryUsage().heapUsed > 500 * 1024 * 1024) {
writeHeapSnapshot();
console.log('Heap snapshot written!');
}
12.4 JVM Memory Debugging
# jmap: Generate heap dump
jmap -dump:format=b,file=heap_dump.hprof <PID>
# jstat: Real-time GC statistics monitoring
jstat -gcutil <PID> 1000
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 45.23 67.89 34.56 97.12 95.00 150 1.234 3 0.567 1.801
# Column meanings:
# S0/S1: Survivor Space utilization
# E: Eden Space utilization
# O: Old Generation utilization
# YGC/YGCT: Young GC count/time
# FGC/FGCT: Full GC count/time (problem if FGC increases!)
VisualVM / Eclipse MAT Analysis
================================
1. Open Heap Dump (hprof file)
2. Check Dominator Tree
- Find objects retaining the most memory
- Shallow Size: size of the object itself
- Retained Size: object + all objects referenced only by it
3. Leak Suspects Report
- Automatically detects suspicious patterns
- "1 instance of X loaded by Y occupies Z bytes"
4. OQL (Object Query Language)
SELECT * FROM java.util.HashMap WHERE size > 10000
-> Find abnormally large collections
13. Common Memory Leak Patterns Summary
13.1 Major Leak Causes by Language
Primary Memory Leak Causes
=============================
JavaScript/Node.js:
1. Data accumulation in global variables
2. Unremoved event listeners
3. Uncleaned setInterval/setTimeout
4. Closures capturing large objects
5. Detached DOM nodes (removed from DOM but referenced by JS)
Java:
1. Unbounded accumulation in static collections
2. Uncleaned ThreadLocal
3. Unclosed connections/streams
4. Custom ClassLoader leaks
5. Unreleased listeners/callbacks
Python:
1. Circular references (solvable with gc.collect())
2. Circular references with __del__ methods (gc may not collect)
3. Memory management errors in C extension modules
4. Unbounded global dictionary growth
Go:
1. Goroutine leaks (non-terminating goroutines)
2. Repeated time.After usage (timer accumulation)
3. Slice retaining underlying array reference
4. sync.Pool misuse
13.2 Detached DOM Nodes (JavaScript)
// Detached DOM leak example
let detachedNodes = [];
function createLeak() {
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.innerHTML = 'content '.repeat(1000);
document.body.appendChild(div);
// Removed from DOM but reference kept in JS array!
document.body.removeChild(div);
detachedNodes.push(div); // Leak!
}
}
// Solution: Remove references too
function cleanup() {
detachedNodes = [];
// Or use WeakRef
}
13.3 Goroutine Leaks (Go)
// Goroutine leak example
func leakyFunction() {
ch := make(chan int)
go func() {
val := <-ch // Waits forever if nobody sends to this channel!
fmt.Println(val)
}()
// Function exits without sending to ch
// goroutine lives forever -> leak!
}
// Solution: Use context for cancellation
func properFunction(ctx context.Context) {
ch := make(chan int)
go func() {
select {
case val := <-ch:
fmt.Println(val)
case <-ctx.Done():
return // goroutine exits on context cancellation
}
}()
}
14. Profiling Tools Comparison
Memory Profiling Tools Comparison
===================================
JavaScript/Node.js:
+----------------------------+------------------------+
| Tool | Purpose |
+----------------------------+------------------------+
| Chrome DevTools Memory | Browser heap analysis |
| node --inspect | Node.js remote debug |
| clinic.js | Performance/memory |
| heapdump package | Programmatic access |
+----------------------------+------------------------+
Java:
+----------------------------+------------------------+
| VisualVM | General profiling |
| Eclipse MAT | Heap dump analysis |
| JFR (Flight Recorder) | Low-overhead profiling |
| async-profiler | CPU+heap profiling |
| jmap/jhat | Command-line analysis |
+----------------------------+------------------------+
Go:
+----------------------------+------------------------+
| pprof | Built-in profiler |
| runtime.ReadMemStats | Runtime statistics |
| go tool trace | Execution tracing |
+----------------------------+------------------------+
Rust:
+----------------------------+------------------------+
| Valgrind (Massif) | Heap profiling |
| heaptrack | Heap tracking |
| DHAT | Dynamic heap analysis |
+----------------------------+------------------------+
General:
+----------------------------+------------------------+
| Valgrind (Memcheck) | Memory error detection |
| AddressSanitizer (ASan) | Fast memory errors |
| LeakSanitizer (LSan) | Leak-only detection |
+----------------------------+------------------------+
15. Quiz
Test your understanding of memory management and garbage collection.
Q1. Why is Mark-and-Sweep GC better than Reference Counting for handling circular references?
A: Reference Counting tracks the reference count of each object, so when A and B reference each other, both maintain a count of 1 or more and are never freed. Mark-and-Sweep, on the other hand, only marks objects reachable from GC Roots (stack, global variables). Even with circular references, if objects are unreachable from GC Roots, they remain unmarked and are collected. In other words, it is based on "reachability," so it accurately identifies only objects in use regardless of circular references.
Q2. What causes frequent Full GC in the JVM, and how do you fix it?
A: Major causes of frequent Full GC include: (1) Old Generation too small, filling up frequently. Increase heap size (-Xmx) or adjust Old ratio. (2) Memory leak causing continuous object accumulation in Old Gen. Analyze heap dump to find leak source. (3) Young Generation too small, causing premature promotion. Adjust -XX:NewRatio or -XX:NewSize. (4) Frequent Humongous allocations filling Old Gen quickly (G1). Increase Region size. Monitor GC patterns with jstat and analyze leaks with heap dumps.
Q3. Why does Rust's ownership system disallow two simultaneous mutable borrows?
A: To prevent data races at compile time. A data race occurs when two or more pointers access the same data simultaneously, at least one performs a write, and access is unsynchronized. Rust's rule of "N immutable references OR 1 mutable reference" structurally eliminates one of these conditions. With only one mutable borrow, no other code can read or write the same data concurrently, guaranteeing both memory safety and thread safety without GC.
Q4. What makes V8's Incremental Marking better than traditional Mark-and-Sweep?
A: Traditional Mark-and-Sweep performs the entire marking phase at once, causing Stop-the-World (STW) pauses of hundreds of milliseconds for large heaps. This causes noticeable jank in 60fps rendering (16.6ms per frame). Incremental Marking splits the marking work into small increments interleaved with application execution. Each marking increment takes only a short time, greatly reducing user-perceived pauses. It is based on tri-color marking which enables suspend/resume, and uses write barriers to accurately track references changed during marking.
Q5. Describe the process of detecting memory leaks using Chrome DevTools' 3-Snapshot Technique.
A: (1) Take the first heap snapshot at the app's initial state. (2) Perform the suspected leaky operation (e.g., navigate to a page and back). (3) Take the second heap snapshot. (4) Repeat the same operation. (5) Take the third heap snapshot. (6) Compare snapshots 2 and 3 to find objects that consistently grow with each repetition -- those are the leaks. Examine the Retainers panel to see which reference chain keeps the object alive, revealing the root cause of the leak.
16. References
- Computer Systems: A Programmer's Perspective - Bryant & O'Hallaron (3rd Edition)
- The Garbage Collection Handbook - Jones, Hosking, Moss (2nd Edition, 2023)
- V8 Blog: Orinoco - https://v8.dev/blog/trash-talk
- JVM GC Tuning Guide - https://docs.oracle.com/en/java/javase/21/gctuning/
- The Rust Programming Language - https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html
- Go GC Guide - https://tip.golang.org/doc/gc-guide
- Chrome DevTools Memory - https://developer.chrome.com/docs/devtools/memory-problems/
- ZGC Documentation - https://wiki.openjdk.org/display/zgc
- jemalloc - https://jemalloc.net/
- Python GC Documentation - https://docs.python.org/3/library/gc.html
- Understanding V8 Memory Structure - https://deepu.tech/memory-management-in-v8/
- Shenandoah GC - https://wiki.openjdk.org/display/shenandoah
- Valgrind Quick Start - https://valgrind.org/docs/manual/quick-start.html