모든 프로그램은 메모리를 쓴다. 그러나 let x = 42 가 어디에 박히는지, new Object() 가 얼마를 지불하는지 대부분 모른 채 작성한다. 이 가이드는 stack vs heap 의 실제 메커니즘, malloc/free 가 OS 와 어떻게 대화하는지, 왜 fragmentation 이 long-running 서버를 죽이는지, RAII (C++/Rust) 와 GC (Java/Go/JS) 의 trade-off 를 정리한다.
Stack vs Heap — 두 메모리 영역
프로세스 가상 주소 공간 (Linux x86-64):
0xFFFFFFFFFFFFFFFF
┌─────────────────┐
│ kernel space │ (커널 전용, 사용자 X)
├─────────────────┤
│ stack (↓) │ ← rsp (현재 함수 frame)
│ │
│ ... │ ← 자유 공간
│ │
│ heap (↑) │ ← brk pointer
├─────────────────┤
│ bss + data │ (전역 변수)
├─────────────────┤
│ text (코드) │
└─────────────────┘
0x0000000000000000Stack 은 위에서 아래로 자라고, heap 은 아래에서 위로. 둘이 만나면 stack overflow / out-of-memory.
Stack — 빠르고 자동 정리
void foo() {
int x = 42; // stack
char buf[1024]; // stack
Point p = {1, 2}; // stack
} // 함수 끝 — rsp 복원, 모든 변수 사라짐 (자동)- 할당 비용 0 — rsp register 를 sub 명령으로 내리기만 하면 끝. 한 cycle.
- 해제 자동 — 함수 return 시 rsp 복원 = 모든 local 변수 사라짐. free() 호출 X.
- 크기 제한 — Linux default 8 MB. 큰 array 박으면 stack overflow.
- scope 한정 — 함수 return 후엔 그 stack frame 못 씀 (return value 로 reference 넘기면 UB).
Heap — 동적 크기·수명
// C
void* ptr = malloc(1024); // heap 에 1024 byte 할당
free(ptr); // 명시적 해제
// C++
auto* p = new Point(1, 2); // heap
delete p; // 명시적 해제
// Rust — RAII
let s = String::from("hi"); // heap (String 의 내부 buffer)
// scope 끝나면 Drop trait 자동 호출 → 해제
// Go / Java / JS — GC
let obj = {x: 1}; // heap
// 사용 끝났는지 runtime 이 추적 → GC 가 해제Heap 은 OS 의 page (4 KB unit) 단위로 받아온 영역을 사용자 라이브러리가 잘게 나눠준다. 그 라이브러리가 malloc 의 구현체 — glibc 의 ptmalloc, Google 의 tcmalloc, Facebook 의 jemalloc 등.
malloc 의 내부 — free list
malloc(1024) 호출 시:
1. allocator 내부 "free list" 검색 — 1024 byte 이상 free chunk 있나?
2. 있으면 → 그 chunk 잘라서 반환 (수십 ns)
3. 없으면 → OS 에 sbrk() 또는 mmap() 으로 새 page 요청 → free list 추가 → 반환
free(ptr) 호출 시:
1. ptr 의 chunk metadata (size 등) 확인
2. 인접 free chunk 와 merge (coalesce) — fragmentation 방지
3. free list 에 추가
→ malloc/free 는 시스템콜 X (대부분). 단순 user-space 라이브러리 작업.Chunk metadata 는 보통 chunk 앞 16 byte. malloc(1024) 호출해도 실제로 1040 byte 사용 — overhead.
Fragmentation — 메모리 누수 아닌데 메모리 부족
초기 heap (free):
[████████████████████████████████] 32 MB free
malloc 패턴:
- 1 KB 할당, 1 KB 할당, 1 KB 할당, ...
- 짝수 번째만 해제
결과:
[██░░██░░██░░██░░██░░██░░██░░██░░] 16 MB free (50%)
↑ ↑ ↑ ↑ ↑ ↑
1KB 구멍들
이제 malloc(2 KB) 요청:
→ 16 MB free 인데 2 KB 연속 공간 없음 → 실패 또는 mmap 으로 새 pageExternal fragmentation — 전체 free 는 충분한데 연속 공간 부족. 장수 서버에서 메모리 사용량이 계속 늘어 보이는 주요 원인 (실제로 누수 X, fragmentation).
대응: jemalloc (size class 별 분리), arena 별 할당 (thread-local), 주기적 restart.
알아두면 좋은 비용
| 연산 | 대략 비용 |
|---|---|
| stack 변수 할당 | ~ 1 ns (rsp sub) |
| malloc (hot, free list hit) | ~ 20-50 ns |
| malloc (new page, mmap) | ~ 1-10 μs (시스템콜) |
| free | ~ 20-50 ns |
| GC minor (young gen) | ~ 1-10 ms (pause) |
| GC major (full collection) | ~ 100 ms - 수 초 (pause) |
RAII vs GC — 해제 책임의 두 갈래
RAII (C++, Rust)
{
let s = String::from("hello"); // heap 할당
let v = vec![1, 2, 3]; // heap 할당
} // scope 끝
// Drop trait 자동 호출 → 컴파일 타임에 박힌 free 호출- 해제 시점 = scope 끝. 결정적·예측 가능.
- GC pause 없음.
- cost = 단순 free 호출 (수십 ns).
- 그러나 ownership / borrow checker 의 정신적 비용 (Rust)
GC (Java, Go, JS, Python)
let obj = {x: 1};
// runtime 이 reachability 추적 → 어느 시점에 unreachable 판정 → 해제- 해제 시점 = GC 가 정함. 비결정적.
- 주기적 pause (mark-and-sweep, generational).
- 프로그래머는 free 잊을 수 없음 — 안전.
- throughput vs latency trade-off (GC 알고리즘 선택).
GC 의 구체 동작은 how-garbage-collection-works 가이드 참조.
실용 패턴 — 빠른 코드를 위한 메모리 의식
1. Stack 우선
가능하면 heap 대신 stack. 작은 array 는 fixed size 로:
// 느림 (heap)
let v = Vec::with_capacity(8); // malloc
// 빠름 (stack, smallvec 또는 array)
let arr: [i32; 8] = [0; 8];2. Pre-allocate
// Java — append 마다 ArrayList 가 grow → 여러 번 malloc + copy
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) list.add(i);
// 좋음 — capacity 미리 박음
List<Integer> list = new ArrayList<>(1_000_000);3. Object pool
Hot path 에서 같은 object 를 반복 할당·해제하면 pool 로 재사용. Go 의 sync.Pool, Netty 의 PooledByteBuf.
4. Arena allocator
Request 단위로 메모리 할당 → request 끝나면 arena 통째로 해제. 개별 free 호출 0. Rust 의 bumpalo, Nginx 의 ngx_pool_t.
흔한 함정
- Use after free — 해제된 pointer 사용. C/C++ 의 UB. heap-buffer-overflow detector (AddressSanitizer) 로 잡음.
- Double free — 같은 pointer 두 번 free. crash 또는 silent corruption.
- Memory leak — free 안 함. 장수 서버에서 누적.
- Stack overflow — 큰 array 를 stack 에 박거나, 깊은 재귀.
- Fragmentation — 위 참조. 누수처럼 보이지만 누수 아님.
마무리
메모리 할당의 cost 는 작지 않다. malloc 50 ns 가 hot path 에서 반복되면 누적된다. stack 우선 / pre-allocate / arena / pool — 네 패턴이 대부분의 hot path 를 커버한다.
그리고 GC 언어를 쓴다고 메모리 의식이 불필요하지 않다 — allocation 패턴이 GC pause 의 빈도·길이를 결정한다. how-garbage-collection-works 에서 그 메커니즘 정리.