Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 28

JavaScript Memory Management

Recommended resources

Hello! Welcome to our next lesson.

In the previous session, we explored how the JavaScript engine creates and manages Execution Contexts on the Call Stack. We learned that this logical structure determines variable accessibility through the Scope Chain.

Today, we'll shift our focus from the logical to the physical: where does the data for our variables actually live, and how is that memory managed? Your goal for this lesson is to explain the high-level process of memory allocation and garbage collection in JavaScript. We will demystify how JavaScript, a language without manual memory management, handles this crucial task under the hood. Given your background in computer science, this lesson will connect familiar concepts like the stack, heap, and collection algorithms to the specific implementation within the V8 engine.

Memory Allocation: The Stack and the Heap

When your JavaScript code runs, the engine uses two primary areas of memory to store data: the Stack and the Heap. Understanding their distinct roles is the first step in understanding memory management.

  • The Stack: This is a region of memory that operates on a Last-In, First-Out (LIFO) basis. It's directly tied to the Call Stack we discussed previously. The stack is used for static memory allocation. This means the engine knows the size of the data at compile time. It stores:

    • Primitive values (string, number, boolean, null, undefined, symbol, bigint).
    • References (or pointers) to objects and functions that are stored on the Heap.
    • Information about each execution context (function arguments, local variables).

    Memory on the stack is fast to allocate and deallocate. When a function is called, its execution context is pushed onto the stack, and when it returns, it's popped off, automatically freeing that memory.

  • The Heap: This is a much larger, less organized region of memory used for dynamic memory allocation. This is where data of unknown size, such as objects, arrays, and functions, is stored. Because the size of these objects can change during runtime (e.g., adding properties to an object or elements to an array), they can't be managed in the orderly fashion of the stack.

JavaScript Stack and Heap Memory with Garbage Collection Illustration
This image illustrates the relationship between the Stack and the Heap. The Stack holds primitive values and references, while the Heap stores the actual objects. When an object is no longer referenced from the Stack or another active object, it becomes "garbage" (the red circle), eligible for collection.

Managing the heap is more complex. While allocation is straightforward (the engine finds a free block of memory), deallocation is not. This is where the Garbage Collector comes in.

Visualised guide to memory management in JavaScript - Kateryna Porshnieva | JSHeroes 2023

To start, let's watch a segment that visualizes how memory is structured and how JavaScript's automatic memory management contrasts with manual approaches found in languages like C. This will set the stage for understanding the specific components of V8's memory model.

Watch the segment from 01:37 to 08:59. Pay close attention to: The basics of how memory is organized in RAM. The distinction between manual memory management (C) and automatic management (JS). How the V8 engine uses the Stack for local context and the Heap for dynamically allocated data like objects.

As the video explained, the V8 engine, which powers Chrome and Node.js, uses this stack/heap architecture. All objects, arrays, and functions you create are allocated on the heap. Variables on the stack hold either a primitive value directly or a reference (an address) to an object on the heap.


The Core of Garbage Collection: Reachability

The central task of a garbage collector (GC) is to identify and reclaim memory that is no longer in use. But how does it know what's "in use"? V8, like most modern engines, determines this based on reachability.

An object is considered reachable if it can be accessed from a set of "root" nodes. These roots include:

  • The global object (window in browsers, global in Node.js).
  • The current call stack (variables and parameters in all active functions).

The GC starts at these roots and traverses all object references. Any object it can reach during this traversal is considered "live." Any object it cannot reach is considered "garbage" and can be safely deallocated.


V8's Generational Garbage Collector

V8's GC team observed a key behavior in most dynamic languages, which they termed the Generational Hypothesis:

  1. Most objects die young. They are allocated for a short-lived task and quickly become unreachable.
  2. Objects that survive for a while tend to live for a long time.

Based on this, V8 splits its heap into two main generations to optimize the collection process:

  • The Young Generation (or "New Space"): This is where all new objects are allocated. It's relatively small and is collected frequently and quickly.
  • The Old Generation (or "Old Space"): Objects that survive a few GC cycles in the Young Generation are "promoted" to the Old Generation. This space is larger and is collected less frequently.
Generational Garbage Collection in JavaScript Engines
This diagram shows the promotion of an object through V8's memory generations. An object starts in the 'nursery' (part of the Young Generation), and if it survives collection cycles, it's promoted first to 'intermediate' and then to the 'old generation'.

This generational approach is highly efficient because the GC can focus most of its effort on the Young Generation, where garbage is most likely to be found, performing quick cleanups without having to scan the entire heap every time.


Collection Algorithms in V8

V8 uses different algorithms for each generation, tailored to their specific characteristics.

1. Minor GC (Scavenger) for the Young Generation

The Young Generation is collected using a fast algorithm called a Scavenger, which implements Cheney's algorithm. The Young Generation is split into two equal semi-spaces: From-space and To-space.

Here's the process:

  1. New objects are allocated in From-space.
  2. When From-space fills up, a Minor GC is triggered.
  3. The Scavenger traverses the live objects in From-space, copying them to To-space.
  4. As objects are copied, they are compacted together, eliminating memory fragmentation.
  5. Once all live objects are moved, the From-space is cleared.
  6. The roles of the two spaces are swapped. The old To-space (now containing the live objects) becomes the new From-space, and the old From-space (now empty) becomes the new To-space.
  7. If an object survives this process twice, it gets promoted to the Old Generation.

This copy-and-compact approach is very fast for small amounts of data with a high percentage of garbage, which is exactly the profile of the Young Generation.

2. Major GC (Mark-Sweep-Compact) for the Old Generation

Copying large amounts of memory is expensive, so the Scavenger algorithm is not suitable for the much larger Old Generation. Instead, V8 uses a Mark-Sweep-Compact algorithm.

  1. Marking: The GC traverses the heap from the root nodes and marks every reachable object as "live."
  2. Sweeping: The GC sweeps through the Old Generation, identifying unmarked (garbage) objects. The memory from these objects is added to a data structure called a free list, which keeps track of available memory chunks of different sizes.
  3. Compacting (Optional): Over time, the heap can become fragmented with small, scattered free spaces. When fragmentation becomes an issue, the GC will perform a compaction step, moving live objects together to create larger, contiguous free memory blocks. This is an expensive operation and is performed selectively.

Visualised guide to memory management in JavaScript - Kateryna Porshnieva | JSHeroes 2023

The following video provides a fantastic visualization of both the Scavenger and the Mark-Sweep-Compact algorithms in action. It clearly explains the generational hypothesis and the step-by-step mechanics of each collector.

Watch from 15:40 to 24:42. Focus on: The concept of reachability and memory fragmentation. The Scavenger algorithm: From-space, To-space, copying, and promotion. The Generational Hypothesis as the reason for two separate collectors. The Mark-Sweep-Compact algorithm: the marking, sweeping (using free lists), and compaction phases.


Making Garbage Collection Performant: The Orinoco Project

A major challenge for garbage collection is the "stop-the-world" pause. In early versions of V8, the main thread would have to stop executing JavaScript to perform GC, leading to noticeable stutters or "jank" in applications.

The Orinoco project is the codename for V8's modern GC pipeline, which employs advanced techniques to minimize these pauses. Since you are interested in the internal architecture, understanding these concepts is key.

Orinoco: The new V8 Garbage Collector Peter Marshall

This talk by a V8 engineer explains the evolution of the garbage collector and defines the techniques used to make it highly performant. This is crucial for understanding how modern JavaScript runtimes can handle demanding applications.

Watch the segment from 11:54 to 18:21. It clearly defines and differentiates between three critical optimization techniques: Parallel GC: Multiple helper threads work together on a GC task during a stop-the-world pause to shorten it. Incremental GC: The GC work is broken into smaller pieces and interleaved with JavaScript execution. Concurrent GC: GC work is done on helper threads at the same time as the main thread executes JavaScript, avoiding a pause altogether.

V8's Orinoco applies these techniques to both Minor and Major GC:

  • Parallel Scavenging: The Minor GC is done in parallel, using multiple threads to speed up the copying process, dramatically reducing the pause time.
  • Concurrent Marking & Sweeping: For the Major GC, marking and sweeping can happen concurrently in the background while your JavaScript code continues to run. Only the final compaction phase might require a brief pause.

These optimizations are why modern web applications and Node.js servers can feel so responsive despite performing constant memory management.


Conclusion

You now have a robust mental model of how JavaScript manages memory. While abstracted away from the developer, this system is a sophisticated piece of engineering designed for performance.

Key Takeaways:

  • Memory is divided into the Stack (for static data like primitives and references) and the Heap (for dynamic data like objects and arrays).
  • Garbage collection is an automatic process that reclaims memory from objects that are no longer reachable from root nodes (like the global object and the call stack).
  • V8 uses a generational collector based on the hypothesis that most objects die young.
    • The Young Generation uses a fast Scavenger (copying) algorithm.
    • The Old Generation uses a Mark-Sweep-Compact algorithm.
  • V8's modern GC, Orinoco, uses parallel, concurrent, and incremental techniques to minimize "stop-the-world" pauses and improve application performance.

What's Next?

We've now covered several core runtime mechanics of the JavaScript engine. Next, we will shift our focus to the language itself and its ecosystem. Our next lesson will explore the purpose of a static type system and how TypeScript enhances JavaScript development, moving from how code runs to how we can write more robust and maintainable code.