0Pricing
Node.js Backend Development Bootcamp · 강의

V8 힙, 세대별 GC 및 객체 수명

V8이 젊은 세대와 오래된 세대에 걸쳐 메모리를 할당하고 회수하는 방식을 이해합니다.

V8 힙, 세대별 GC 및 객체 수명은(는) CoddyKit의 무료 Node.js Backend Development Bootcamp 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Node.js Backend Development Bootcamp 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Node.js Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Why Heap Mechanics Matter in Node

Every object, closure, and buffer your Node.js service allocates lives somewhere in V8's managed memory. Understanding where it lives and when it gets reclaimed is the difference between a service that holds steady at 200 MB and one that creeps toward an OOM crash under load.

V8 splits its managed memory into two main regions:

  • New space (young generation) — small, fast, where almost every object is born.
  • Old space (old generation) — large, where objects that survive go to live.

You can inspect the live numbers at runtime with process.memoryUsage().

// heap_snapshot.js — print V8 heap usage in MB
function mb(bytes) {
  return (bytes / 1024 / 1024).toFixed(2) + ' MB';
}

const u = process.memoryUsage();
console.log('rss        ', mb(u.rss));        // total process memory
console.log('heapTotal  ', mb(u.heapTotal));  // V8 heap reserved
console.log('heapUsed   ', mb(u.heapUsed));   // V8 heap live
console.log('external   ', mb(u.external));   // C++ objects bound to JS
console.log('arrayBuffers', mb(u.arrayBuffers));

The Weak Generational Hypothesis

V8's GC is built on a single empirical observation called the weak generational hypothesis: most objects die young.

In a typical request handler, you allocate request-scoped objects — parsed JSON bodies, temporary arrays, intermediate strings — that become garbage the instant the response is sent. A small minority (caches, connection pools, module-level singletons) survive for the lifetime of the process.

This skew lets V8 optimize aggressively: it collects the young generation frequently and cheaply, and the old generation rarely and thoroughly. Each generation gets a GC algorithm tuned to its survival profile.

New Space: Scavenge and Semi-Spaces

New space is collected by the Scavenger, a copying collector using Cheney's algorithm. New space is split into two equal halves called semi-spaces: to-space and from-space.

  • New objects are bump-allocated into to-space (just advance a pointer — extremely fast).
  • When to-space fills, a minor GC (scavenge) runs: live objects are copied into from-space, the roles flip, and the old to-space is wiped wholesale.

Because cost is proportional to surviving objects (not garbage), and most objects are dead by collection time, scavenges are very cheap — typically under a millisecond.

Allocating in the Young Generation

Watch the young generation in action. The loop below allocates millions of short-lived objects. Each one dies almost immediately, so they never leave new space — the Scavenger reclaims them in tight, cheap cycles and heapUsed stays roughly flat.

This is the ideal allocation pattern for a request handler: allocate freely, let objects die fast, and the GC barely notices.

// young_gen.js — millions of short-lived allocations
function work() {
  let acc = 0;
  for (let i = 0; i < 5_000_000; i++) {
    // {x, y} is born and dies within the iteration
    const point = { x: i, y: i * 2 };
    acc += point.x + point.y;
  }
  return acc;
}

const before = process.memoryUsage().heapUsed;
const result = work();
const after = process.memoryUsage().heapUsed;
console.log('result:', result);
console.log('heapUsed delta (MB):', ((after - before) / 1024 / 1024).toFixed(2));

Promotion: Surviving Into Old Space

An object that survives a scavenge is not immediately old. V8 tracks survival per object:

  • An object surviving its first scavenge is copied to from-space (still young, but now in an intermediate state).
  • An object that survives a second scavenge is promoted (tenured) into old space.

So objects referenced long enough to live through two minor GCs graduate to the old generation. There is also an aggressive promotion path: if to-space is more than ~80–90% full after a scavenge, V8 promotes survivors early to avoid thrashing the small semi-space.

Forcing Promotion With a Retained Reference

The pattern below retains every allocated object in a long-lived array. Those objects survive repeated scavenges, get promoted to old space, and heapUsed climbs steadily. This is exactly what an unbounded in-memory cache looks like — and how a slow leak begins.

The lesson: retention, not allocation rate, is what fills old space. A handler that allocates a million objects but keeps none costs almost nothing; one that keeps a thousand forever leaks.

// promotion.js — retained objects get tenured into old space
const retained = [];

for (let i = 0; i < 1_000_000; i++) {
  // pushing keeps a live reference -> survives scavenges -> promoted
  retained.push({ id: i, payload: 'item-' + i });
}

const u = process.memoryUsage();
console.log('retained length:', retained.length);
console.log('heapUsed (MB):', (u.heapUsed / 1024 / 1024).toFixed(2));
console.log('heapTotal (MB):', (u.heapTotal / 1024 / 1024).toFixed(2));

Old Space: Mark-Sweep-Compact

Old space is collected by a major GC using Mark-Sweep-Compact:

  • Mark — traverse the object graph from the GC roots (global object, the execution stack, native handles) and mark everything reachable.
  • Sweep — add the memory of unmarked (dead) objects to free lists for reuse.
  • Compact — occasionally relocate live objects to defragment, so large allocations can find contiguous space.

Major GC is far more expensive than a scavenge because it must walk the entire old generation. To avoid long pauses, V8 runs most of marking concurrently on background threads and incrementally interleaved with your JS — this is the Orinoco GC architecture.

Reachability Defines Garbage

An object is garbage if and only if it is unreachable from the GC roots. V8 does not use reference counting, so cycles are collected correctly — two objects pointing at each other are still garbage if nothing reachable points to either.

The corollary for backend code: a memory leak is almost always an accidental reference that keeps an object reachable. Common culprits are module-level Maps, event listeners never removed, and closures capturing large scopes.

// reachability.js — an unintentional cycle is still collectable
function makePair() {
  const a = {};
  const b = {};
  a.peer = b;   // a -> b
  b.peer = a;   // b -> a (cycle)
  return a;
}

let root = makePair();
console.log('reachable via root:', root.peer.peer === root);

root = null; // drop the only root reference
// Both a and b are now unreachable despite the cycle;
// V8 will reclaim them on the next GC. No leak.
console.log('root dropped; cycle is now garbage');

Sizing the Heap: --max-old-space-size

The old generation has a configurable ceiling. On 64-bit Node, the default old-space limit is roughly 2 GB (older versions defaulted near 1.5 GB; modern Node scales it to available memory). When old space cannot grow further and a major GC fails to free enough, the process dies with FATAL ERROR: ... JavaScript heap out of memory.

You raise the ceiling with the V8 flag --max-old-space-size (in MB):

  • node --max-old-space-size=4096 server.js sets a 4 GB old-space cap.

Raising it buys headroom but is not a fix for a leak — a leak will simply hit the new, higher limit later. Always pair sizing with leak diagnosis.

Observing GC With perf_hooks

You can directly observe minor vs. major GC events using the built-in perf_hooks PerformanceObserver with entryTypes: ['gc']. Each entry's detail.kind tells you the collection type:

  • NODE_PERFORMANCE_GC_MINOR — a scavenge (young generation).
  • NODE_PERFORMANCE_GC_MAJOR — a mark-sweep-compact (old generation).

Use this in load tests to confirm whether your service is dominated by cheap minor GCs (healthy) or frequent expensive major GCs (a retention problem).

// gc_observer.js — log each GC event and its duration
const { PerformanceObserver, constants } = require('perf_hooks');

const kinds = {
  [constants.NODE_PERFORMANCE_GC_MINOR]: 'minor (scavenge)',
  [constants.NODE_PERFORMANCE_GC_MAJOR]: 'major (mark-sweep)',
  [constants.NODE_PERFORMANCE_GC_INCREMENTAL]: 'incremental',
  [constants.NODE_PERFORMANCE_GC_WEAKCB]: 'weak-callback',
};

const obs = new PerformanceObserver((list) => {
  for (const e of list.getEntries()) {
    console.log(kinds[e.detail.kind] || 'other', '-', e.duration.toFixed(2), 'ms');
  }
});
obs.observe({ entryTypes: ['gc'] });

// generate garbage to trigger collections
let sink = [];
for (let i = 0; i < 2_000_000; i++) {
  sink.push({ i });
  if (sink.length > 50_000) sink = [];
}
console.log('done allocating');

Designing for the Generational GC

Concrete practices that keep your service friendly to V8's generational collector:

  • Let request data die young. Avoid stashing per-request objects in module-level structures; they get promoted and you pay major-GC cost forever.
  • Bound your caches. Use an LRU with a max size instead of an unbounded Map, so old space stays flat.
  • Prefer WeakMap/WeakRef for associative metadata keyed by objects you don't own — entries vanish when the key becomes unreachable.
  • Reuse big buffers via pools rather than re-allocating; large allocations stress old space and compaction.

The mental model: cheap, disposable young-generation churn is fine; long-lived growth in old space is what you must watch.

// weakmap_metadata.js — metadata that auto-collects with its key
const lastSeen = new WeakMap();

function touch(session) {
  lastSeen.set(session, Date.now()); // no strong ref to session
  return lastSeen.get(session);
}

let session = { id: 'abc' };
console.log('touched at:', touch(session));

session = null; // session unreachable -> WeakMap entry eligible for GC
console.log('session released; WeakMap entry will be reclaimed');

Quick Check: Object Lifetime

A reasoning question about how an object travels through V8's generations.

Recap: Heap, Generations, Lifetimes

What to carry forward:

  • Two generations. New space (young) holds freshly allocated objects; old space (old) holds survivors.
  • Weak generational hypothesis. Most objects die young, so V8 collects new space often and cheaply, old space rarely and thoroughly.
  • Scavenge. Minor GC copies live objects between semi-spaces; cost scales with survivors, not garbage.
  • Promotion. Surviving two scavenges (or filling to-space) tenures an object into old space.
  • Mark-Sweep-Compact. Major GC traces from roots, frees the unreachable, and occasionally compacts — run mostly concurrently/incrementally.
  • Leaks are retention. Unreachable means collectable, even through cycles; accidental long-lived references are what fill old space and cause OOM.
  • Tools. process.memoryUsage(), perf_hooks GC entries, and --max-old-space-size let you measure, observe, and size the heap.

자주 묻는 질문

“V8 힙, 세대별 GC 및 객체 수명” 강의는 무료인가요?

네 — “V8 힙, 세대별 GC 및 객체 수명” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Node.js Backend Development Bootcamp 강의 전체를 잠금 해제할 수 있습니다. Node.js Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.

“V8 힙, 세대별 GC 및 객체 수명”에서 뭘 배우나요?

V8이 젊은 세대와 오래된 세대에 걸쳐 메모리를 할당하고 회수하는 방식을 이해합니다. 브라우저에서 직접 실행하는 실습 코드로 Node.js Backend Development Bootcamp을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Node.js Backend Development Bootcamp을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Node.js Backend Development Bootcamp은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.

“V8 힙, 세대별 GC 및 객체 수명” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Node.js Backend Development Bootcamp 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Node.js Backend Development Bootcamp 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. V8 힙, 세대별 GC 및 객체 수명
  2. 힙 스냅샷 캡처 및 비교
  3. 핫 경로를 위한 CPU 프로파일링 및 플레임 그래프
  4. 일반적인 누수 패턴 탐지 및 해결
← Node.js Backend Development Bootcamp(으)로 돌아가기