Skip to main content

Memory Management

Background

Most of the time, you can probably get by fine not knowing anything about memory management as a JavaScript developer. After all, the JavaScript engine handles this for you.

At one point or another, though, you'll encounter problems, like memory leaks, that you can only solve if you know how memory allocation works.

In this article, I'll introduce you to how memory allocation and garbage collection works and how you can avoid some common memory leaks.

Memory life cycle

In JavaScript, when we create variables, functions, or anything you can think of, the JS engine allocates memory for this and releases it once it's not needed anymore.

Allocating memory is the process of reserving space in memory, while releasing memory frees up space, ready to be used for another purpose.

Every time we assign a variable or create a function, the memory for that always goes through the same following stages:

img_1.png

Allocate memory

JavaScript takes care of this for us: It allocates the memory that we will need for the object we created.

Use memory

Using memory is something we do explicitly in our code: Reading and writing to memory is nothing else than reading or writing from or to a variable.

Release memory

This step is handled as well by the JavaScript engine. Once the allocated memory is released, it can be used for a new purpose.

tip

"Objects" in the context of memory management doesn't only include JS objects but also functions and function scopes.

Memory stack and heap

We now know that for everything we define in JavaScript, the engine allocates memory and frees it up once we don't need it anymore.

The next question that came to my mind was: Where is this going to be stored?

JavaScript engines have two places where they can store data: The memory heap and stack.

These are two data structures that the engine uses for different purposes.

Stack: Static memory allocation

All the values get stored in the stack since they all contain primitive values.

img_2.png

A stack is a data structure that JavaScript uses to store static data. Static data is data where the engine knows the size at compile time. In JavaScript, this includes primitive values (strings, numbers, booleans, undefined, and null) and references, which point to objects and functions.

Since the engine knows that the size won't change, it will allocate a fixed amount of memory for each value.

The process of allocating memory right before execution is known as static memory allocation.

Because the engine allocates a fixed amount of memory for these values, there is a limit to how large primitive values can be.

The limits of these values and the entire stack vary depending on the browser.

Heap: Dynamic memory allocation

The heap is a different space for storing data where JavaScript stores objects and functions.

Unlike the stack, the engine doesn't allocate a fixed amount of memory for these objects. Instead, more space will be allocated as needed.

Allocating memory this way is also called dynamic memory allocation.

Overview

To get an overview, here are the features of the two storages compared side by side:

StackHeap
Primitive values and referencesObjects and functions
Size is known at compile timeSize is known at runtime
Allocates a fixed amount of memoryAllocates more memory as needed (No limit per object)

Example

Let's have a look at a few code examples. In the captions I mention what is being allocated:

const person = {
name: "John",
age: 24,
};

JS allocates memory for this object in the heap. The actual values are still primitive values, so they are stored in the stack. The reference to the object is stored in the stack.

And by actual values, I mean the values of the properties of the object. The object itself is stored in the heap.

const hobbies = ["hiking", "reading"];

Arrays are objects as well, which is why they are stored in the heap.

let name = "John"; // allocates memory for a string
const age = 24; // allocates memory for a number

name = "John Doe"; // allocates memory for a new string
const firstName = name.slice(0, 4); // allocates memory for a new string

Primitive values are immutable, which means that instead of changing the original value, JavaScript creates a new one.

References in JavaScript

All variables first point to the stack. In case it's a non-primitive value, the stack contains a reference to the object in the heap.

The memory of the heap is not ordered in any particular way, which is why we need to keep a reference to it in the stack. You can think of references as addresses and the objects in the heap as houses that these addresses belong to.

img_3.png

In this picture, we can observe how different values are stored. Note how person and newPerson both point to the same object.

For example:

const person = {
name: "John",
age: 24,
};

This creates a new object in the heap and a reference to it in the stack.

Garbage collection

We now know how JavaScript allocates memory for all kinds of objects, but if we remember the memory lifecycle, there's one last step missing: releasing memory.

Just like memory allocation, the JavaScript engine handles this step for us as well. More specifically, the garbage collector takes care of this.

Once the JavaScript engine recognizes that a given variable or function is not needed anymore, it releases the memory it occupied.

The main issue with this is that whether or not some memory is still needed is an undecidable problem, which means that there can't be an algorithm that's able to collect all the memory that's not needed anymore in the exact moment it becomes obsolete.

Some algorithms offer a good approximation to the problem. I'll discuss the most used ones in this section: The reference-counting garbage collection and the mark and sweep algorithm.

Reference-counting garbage collection

This one is the easiest approximation. It collects the objects that have no references pointing to them.

Let's have a look at the following example. The lines represent references.

import ReactPlayer from "react-player";

Note how in the last frame only hobbies stays in the heap since it's the object one that has a reference in the end.

This algorithm is very fast, but it has a major drawback: It can't handle circular references.

const person = {
name: "John",
age: 24,
};

person.hobbies = ["hiking", "reading"];

person.hobbies.push(person);

In this example, person has a reference to hobbies and hobbies has a reference to person. This means that the algorithm can't detect that person is not needed anymore.

Mark and sweep algorithm

This algorithm is a bit more complex. It first marks all the objects that are still needed and then sweeps through the heap and removes all the objects that are not marked.

This algorithm is more complex, but it can handle circular references.

Memory leaks

Armed with all this knowledge about memory management, let's have a look at the most common memory leaks.

You will see that these can be easily avoided if one understands what is going on behind the scenes.

Global variables

Storing data in global variables is probably the most common type of memory leak.

For example if you have a global variable (declare variable using var instead of let or const) that stores a list of users, it will never be released from memory.

var users = ["John", "Jane", "Mary"];

The same will happen to functions that are defined with the function keyword.

var users = ["John", "Jane", "Mary"];

function getUser() {
return users[0];
}

var secondUser = getUser();

All three variables users, getUser and secondUser are stored in the global scope. This means that they will never be released from memory.

Avoid this by running your code in strict mode. This will make sure that you can't declare variables in the global scope.

"use strict";

var users = ["John", "Jane", "Mary"];

Apart from adding variables accidentally to the root, there are many cases in which you might do this on purpose.

You can certainly make use of global variables, but make sure you free space up once you don't need the data anymore.

To release memory, assign the global variable to null.

users = null;

Forgotten timers and callbacks

Forgetting about timers and callbacks can make the memory usage of your application go up. Especially in Single Page Applications (SPAs), you have to be careful when adding event listeners and callbacks dynamically.

Forgotten timers

const object = {};
const intervalId = setInterval(function () {
// everything used in here can't be collected
// until the interval is cleared
doSomething(object);
}, 2000);

The code above runs the function every 2 seconds. If you have code like this in your project, you might not need this to run all the time.

The objects referenced in the interval won't be garbage collected as long as the interval isn't canceled.

Make sure to clear the interval once it's not needed anymore.

clearInterval(intervalId);

This is especially important in SPAs. Even when navigating away from the page where this interval is needed, it will still run in the background.

Forgotten callbacks

Let's say you add an onclick listener to a button, which later on gets removed.

Old browsers weren't able to collect the listener, but nowadays, this isn't a problem anymore.

Still, it's a good idea to remove event listeners once you don't need them anymore:

const element = document.getElementById("button");
const onClick = () => alert("hi");

element.addEventListener("click", onClick);

element.removeEventListener("click", onClick);
element.parentNode.removeChild(element);

Out of DOM reference

This memory leak is similar to the previous ones: It occurs when storing DOM elements in JavaScript.

const elements = [];
const element = document.getElementById("button");
elements.push(element);

function removeAllElements() {
elements.forEach((item) => {
document.body.removeChild(document.getElementById(item.id));
});
}

When you remove any of those elements, you'll probably want to make sure to remove this element from the array as well.

Otherwise, these DOM elements can't be collected.

const elements = [];
const element = document.getElementById("button");
elements.push(element);

function removeAllElements() {
elements.forEach((item, index) => {
document.body.removeChild(document.getElementById(item.id));
elements.splice(index, 1);
});
}

Removing the element from the array keeps it in sync with the DOM.

Since every DOM element keeps a reference to its parent node as well, you'll prevent the garbage collector from collecting the element's parent and children.

Conclusion

In this article, I summarized the core concepts of memory management in JavaScript. Writing this article helped me clear up some concepts that I didn't understand completely, and I hope this will serve as a good overview of how memory management works in JavaScript.