6.0 런타임과 메모리 — 코드와 운영체제 사이의 숨은 실행 계층
작성한 코드와 그것을 실행하는 운영체제 사이에는 함수 호출을 실행하고 메모리를 할당·회수하는 계층 — 런타임 — 이 있다. 이 인트로는 스택 트레이스, 메모리 증가, GC 일시정지를 서로 무관한 증상이 아니라 런타임이 실행과 메모리를 관리한 결과로 읽는 관점, 즉 챕터 6 전체가 쓰는 지도를 세운다.
학습 목표
- 크래시·메모리 증가·지연 스파이크 앞에서 코드, 런타임(호출·할당·회수), 커널 중 어느 계층을 관찰할지 고른다.
- 함수 호출과 메모리 할당이 "공짜 문법"이 아니라 각각 스택 프레임과 할당자 알고리즘이라는 실행 비용을 갖는 동작임을 설명한다.
- 수동 관리·소유권·GC를 진영이 아니라 "해제 시점을 누가 언제 결정하는가"의 트레이드오프 축 위에서 비교한다.
출발점: 세 개의 증상, 하나의 계층
5년차 이상 개발자라면 다음 셋을 각각 만나 봤을 것이다.
- 끊긴 스택 트레이스 — 프로덕션 에러의 트레이스가
processTicksAndRejections에서 끊겨 있거나, 릴리스 빌드의 크래시 리포트에서 중간 프레임이 사라져 있다. - 떨어지지 않는 메모리 그래프 — 대시보드의 RSS가 계단식으로 오르기만 한다. leak인가? 그런데 힙 스냅샷의 heap used는 멀쩡하다. 두 숫자는 왜 다른가?
- 주기적인 p99 스파이크 — 평균 지연은 좋은데 꼬리 지연이 주기적으로 튄다. GC 로그를 열어 보라는 조언을 듣지만, 로그의
Scavenge와Mark-Compact중 무엇이 문제이고 무엇을 조정해야 하는지 모른다.
세 증상은 도구도 다르고(디버거, 대시보드, GC 로그) 담당자도 다르지만, 전부 같은 계층에서 만들어진다. 스택 트레이스는 런타임이 함수 호출을 실행하려고 쌓은 스택 프레임의 기록이고, RSS와 heap used의 간극은 할당자가 커널에서 받아온 메모리를 보유하는 정책이며, p99 스파이크는 회수기가 살아 있는 객체를 판별하는 동안 프로그램을 세운 시간이다. 이 계층의 구조를 모르면 세 증상은 각각의 미신("트레이스는 가끔 이상하다", "Node는 메모리를 안 돌려준다", "GC는 원래 튄다")으로 남고, 알면 세 증상 모두 관찰하고 조정할 수 있는 메커니즘이 된다.
관점: 층을 나눠야 원인이 보인다
이 챕터가 반복해서 쓸 진단 프레임은 층의 분리다. "메모리가 오른다"는 하나의 지표 뒤에 최소 세 층이 있다.
코드 계층 객체를 참조로 붙들고 있는가? (도달 가능한 leak — 6.3)
│
런타임 계층 할당자가 해제된 메모리를 재사용하려고 보유하는가? (RSS ≠ heap used — 6.2)
│ GC가 아직 회수하지 않았을 뿐인가? (힙 여유와 회수 시점 — 6.3)
│
커널 계층 페이지가 실제로 언제 매핑·회수되는가? (챕터 8에 위임)같은 지표라도 층마다 처방이 다르다 — 코드 층이면 참조를 끊어야 하고, 런타임 층이면 정상이거나 설정 문제이며, 커널 층이면 컨테이너 메모리 제한 같은 다른 규칙이 지배한다. 층을 구분하지 않은 처방("힙을 늘리자", "GC를 자주 돌리자")이 자주 빗나가는 이유다. 실행 쪽도 같다: "이 함수가 느리다"의 뒤에는 코드의 알고리즘, 런타임의 실행 전략(챕터 5의 JIT), 그리고 이 챕터가 다루는 호출·할당·회수 비용이 겹쳐 있다.
학습 지도 — 호출, 할당, 회수
본문 세 편은 런타임의 세 책임을 하나씩 맡고, 앞 문서가 세운 모델을 뒤 문서가 재사용한다.
6.1 런타임 시스템 — 함수 호출은 어떻게 실행되는가
스택 트레이스와 크래시 덤프는 어떤 구조 위에서 만들어지고, 어디까지 믿을 수 있는가? 프로세스 주소 공간의 지도, 호출 규약이라는 ABI 계약, 스택 프레임 체인을 세우고 — 트레이스가 그 체인을 걷는 기록임을 디버거로 직접 확인한다. 재귀 한계 실측, 링킹과 심볼 해석, zero-cost 예외의 비용 위치, 그리고 인라인화·async 경계에서 트레이스가 끊기는 구조적 이유까지. 여기서 세운 스택 모델이 6.2(스택 vs 힙의 경계)와 6.3(root set 스캔)의 전제다.
6.2 메모리 관리 — 힙 할당자와 소유권
malloc은 내부에서 무엇을 하고, 왜 총 여유 메모리가 충분한데도 할당이 실패하거나 느려지는가? free list와 size class, splitting·coalescing, 멀티스레드를 위한 arena 구조를 세우고, 외부 단편화를 시뮬레이션으로 직접 관찰한다. 해제가 OS 반환을 뜻하지 않는 이유(RSS vs heap used)를 실측하고, 수동 관리의 오류 유형(use-after-free, double free, leak)과 그것을 컴파일 타임으로 옮기는 소유권 모델(RAII, Rust 차용 검사)의 트레이드오프를 다룬다.
6.3 가비지 컬렉션 — 자동 메모리 관리의 비용 구조
GC 일시정지는 왜 생기고, 각 설계는 지연 시간과 처리량 중 무엇을 사고 무엇을 파는가? 도달성(root set에서의 그래프 탐색)이라는 "살아 있음"의 정의에서 출발해, 참조 계수와 추적, mark-sweep·mark-compact·copying, 세대 가설과 write barrier, 동시 GC의 tri-color invariant까지 비용 구조로 비교한다. node --trace-gc 실측으로 할당 패턴과 힙 설정이 일시정지에 미치는 영향을 확인하고, V8·Go·JVM·CPython을 트레이드오프 지도 위에 배치한다.
이 챕터가 다루지 않는 것: 가상 메모리와 페이지 폴트의 메커니즘은 8.2가, 캐시·메모리 계층이 성능에 미치는 영향은 7.2가, 바이트코드 VM과 JIT은 5.3이 맡는다. 어셈블리 문법 교육이나 GC 튜닝 플래그 카탈로그도 만들지 않는다 — 목표는 특정 런타임의 설정법이 아니라, 어떤 런타임을 만나도 그 문서를 읽고 판단할 수 있는 비용 모델이다.
정리
- 스택 트레이스·메모리 증가·GC 일시정지는 별개 증상이 아니라 런타임이 호출·할당·회수를 관리한 결과다. 이 계층의 구조를 알면 미신이 메커니즘이 된다.
- 진단의 첫 질문은 층의 구분이다 — 코드(참조), 런타임(할당자·GC), 커널(페이지) 중 어느 층의 현상인가에 따라 처방이 갈린다.
- 세 편의 순서는 의존 관계다: 호출과 스택(6.1)이 좌표계를 세우고, 그 위에서 힙 할당자(6.2)를, 그 위에서 회수기(6.3)를 다룬다.
확인 문제
1. 컨테이너로 배포된 Node.js 서비스의 RSS가 배포 후 며칠에 걸쳐 완만히 오르다가 어느 수준에서 멈췄다. 동료가 "메모리 leak이니 조사하자"고 한다. 층 구분 프레임으로 leak이라 단정하기 전에 확인할 것을 층별로 하나씩 제시하라.
정답과 해설
코드 층: 힙 스냅샷에서 heap used 자체가 계속 자라는지 — 자란다면 도달 가능한 참조가 쌓이는 진짜 leak 후보다. 런타임 층: heap used는 평평한데 RSS만 올랐다면, 할당자·GC가 재사용을 위해 커널에서 받은 페이지를 보유하는 정상 동작일 수 있다(6.2에서 실측). 커널 층: 오르다 멈춘 모양은 힙 상한이나 컨테이너 제한 아래에서 안정화된 것일 수 있으므로, 제한 대비 여유와 OOM 재시작 이력을 본다. "RSS 증가 = leak"은 세 층을 하나로 뭉갠 결론이다.
2. 같은 알고리즘을 구현한 코드가 C++에서는 크래시 덤프로, Node.js에서는 RangeError로, Go에서는 아무 문제 없이 끝났다. 세 결과의 차이를 만든 것은 알고리즘이 아니다 — 무엇인가?
정답과 해설
런타임의 정책이다. 같은 깊은 재귀라도, C++은 OS 스택을 넘어 가드 페이지를 치면 복구 불가능한 SIGSEGV(크래시 덤프)가 되고, V8은 자체 스택 한계를 검사해 넘기 전에 복구 가능한 RangeError로 바꾸며, Go는 스택을 힙으로 복사해 키우는 가변 스택이라 한계 자체가 사실상 사라진다(각각 6.1에서 다룬다). 소스 코드가 같아도 크래시와 지연의 특성은 런타임이 호출·메모리를 관리하는 정책의 함수라는 것 — 이 챕터의 핵심 관점이다.
참고 자료
- Jones, Hosking, Moss, The Garbage Collection Handbook, 2nd ed. (2023) — 자동 메모리 관리 전 영역의 표준 참조. 6.3의 용어 기준이며, 챕터 전체의 배경 서가다.
- Doug Lea, A Memory Allocator — dlmalloc 설계 문서. 6.2가 다루는 할당자 문제 정의의 고전적 출발점이다.
- v8.dev — Trash talk: the Orinoco garbage collector — 이 챕터의 주 관찰 대상인 V8 GC 구조의 1차 자료. 6.3의 예습 자료로 적합하다.