Memory Management and Garbage Collection in JavaScript

Memory Management and Garbage Collection in JavaScript

·

6 min read

In this post you can get some info about memory management and garbage collection in JavaScript.

Memory Flow

Memory management in JavaScript has its own lifecycle, consisting of three main stages:

  • Allocation

  • Usage

  • Release

Allocating memory means reserving space into memory. It happens whenever we declare variables, methods or anything else. It is the JavaScript engine - which we will talk about in another post - that will occupy some space into our RAM for anything we decide to create.

Using memory means reading or writing into an allocated memory slot. It happens whenever we assign or change values to variables and when we pass an argument to a function.

Releasing memory means freeing up space whenever a resource is not needed anymore.

Allocation: Stack & Heap

The JavaScript engine makes use of two data structures to store data: the stack and the heap. The former is used for storing static data, while the latter is for dynamic data storing.

Stack

You might have already heard of the stack while exploring JavaScript. We talked about it in the context of the Event Loop. The stack stores static data. But what is stored in the stack, specifically? What does "static" actually mean?

👉STATIC DATA: data that will occupy a fixed slot of memory for each item. The engine knows the size of the data at compile time. The stack stores primitive values (string, number, boolean, null, undefined) and references.

A little note here, JavaScript is an interpreted language by design, but engines like V8 (which is used in Chrome) use JIT ("just-in-time") compilation too. You can read more about it in this article.

Heap

The heap stores dynamic data. For these data, the engine can't allocate in advance a fixed slot of memory.

👉DYNAMIC DATA: objects, functions and function scopes. The necessary amount of space for these objects is known by the engine at run time. So the amount of memory allocated will increase as needed.

Usage: Primitive & Non-Primitive Values

We can consider two main regions of memory in JavaScript: the stack and the heap. Since the heap is not organized in any way, it is the stack that keeps a reference to every object stored in the heap.

Primitive Data

Primitive data are immutable, which is why their allocation is fixed and known in advance. Let's see an example:

// Primitives: Assignment by value
    const x = 10;
    let y = x;
    y = y * 2;

/* OUTPUT: */
    x = 10, y = 20

When manipulating primitive data, JavaScript does not change the original variable's value. Instead, the engine creates a new slot in memory, with its own address. This is said assignment by value.

References

One of the main differences between primitive types and objects is that objects are memorized and copied by reference, and not by value.

 // Objects: Assignment by reference
    const person = {                 
        age: 30,
        name: "John",
    }
    const friend = person;
    friend.name = "Maria";

  /* OUTPUT: */
      person = {
          age: 30,
          name: "Maria" 
      }
      friend = {
          age: 30,
          name: "Maria" 
      }

In the example above, when changing the friend's object "name" field, we are modifying the same field inside the "person" object too, as they both point to the same memory address. Indeed, when valorized with an object that already exists, a variable is strongly holding that original object, as it points to the same memory address.

This means that when we assigned the "person" object to the "friend" variable, JavaScript did not allocate a brand-new spot, but instead it added a reference to the original memory address.

Cloning

Assignment by reference can lead to clumsy errors, but it is fairly easy to prevent eventual bugs linked to this, using for example the spread operator, the Object.assing() method or the structuredClone method. In that way, you will clone the original object, preserving its memory space from being touched. An example:

 // Objects: Cloning with Spread Operator
    const person = {                 
        age: 30,
        name: "John",
    }
    const friend = { ...person }; // spread operator
    friend.name = "Maria";

  /* OUTPUT: */
    person = {
        age: 30,
        name: "John" 
    }
    friend = {
        age: 30,
        name: "Maria" 
    }

Release: The Garbage Collector

Finally, when the allocated resource is not needed anymore, it can be released. Nonetheless, deciding when it is okay to release a resource is an undecidable problem. As per the mdn web docs:

"[...] finding whether some memory "is not needed anymore" is undecidable."

This issue and the whole release process is handled by the garbage collector.

👉GARBAGE COLLECTOR: the garbage collector is in charge of detecting whether an allocated memory slot can be emptied and eventually free it up.

Modern browsers fundamentally use the "mark-and-sweep" algorithm, a powerful algorithm that solves the circular dependecies issue of the previously used one, the "reference-counting" algorithm.

Mark-and-Sweep Algorithm

The mark-and-sweep algorithm checks if an object's references are reachable from the root object, i.e. the window, in the browser. Root objects are not considered for garbage collection, but if any other object than the root is not reachable, it gets collected and released.

That is, an object is considered not needed anymore when nobody is pointing to it. Visually, it is like a tree of nodes: every object is a node and the window object is at the top of them all. Each node can have its own references. Only the nodes that are not referenced in any way will be removed.

There are also some improved versions of the mark-and-sweep algorithm. One of them is the generational algorithm, where objects are grouped by generations, basing upon how frequently they are being accessed. The more an object is accessed, the younger will be the generation to which it belongs. However, the mark-and-sweep algorithm remains at the core of JavaScript garbage collection mechanism.

WeakMaps & WeakSets

Although JavaScript handles the garbage collection process implicitly - unlinke lower-level programming languages like C - it provides some special data structures to improve memory usage.

For instance, WeakMaps and WeakSets mirror their respective "strong" versions: Map and Set. The main difference is that the keys of WeakMap and WeakSet can be garbage-collected, as long as nothing else is referencing them. If you want to learn more about these data structures, check out the docs here and here.

Memory Leaks

There is the chance you will run into a memory leak when developing in JavaScript. For instance, if you forget to clear a timer or if you are manipulating the DOM adding event listeners but never removing them.

What is actually happening under the hood is that something in your program keeps referencing an object even if in fact it is not needed anymore. This prevents the garbage collector to see it as unreachable, resulting in allocating more memory than needed.

Infinite loops, closures, undeclared or global variables are other common causes of memory leaks. The good news is that it is fairly straight to address and fix a memory leak, once you have detected it! In that regard, when deepening this topic, this Chrome DevTools' article did it for me: have a look!

Sources, Tools & Suggestions

Did you find this article valuable?

Support mydevnook by becoming a sponsor. Any amount is appreciated!