top of page

JavaScript Memory Management Explained

As a JavaScript developer, you don't need to worry about memory management most of the time, as it is handled by the JavaScript engine. However, understanding how memory allocation and garbage collection work can help you troubleshoot and prevent memory-related issues such as memory leaks.


Let's explore the life cycle of memory in JavaScript:

JavaScript Memory Management

  1. Allocation: When you create variables, functions, or any other data structure in JavaScript, the JavaScript engine allocates memory to store them. This process involves reserving space in memory to hold the values.

  2. Usage: Once memory is allocated, you can use it to store and manipulate data. Variables hold references to the allocated memory, allowing you to read from or modify the stored values.

  3. Release: Memory that is no longer needed should be released to free up resources. In JavaScript, memory release is automatically handled by the garbage collector, a component of the JavaScript engine. The garbage collector identifies unreferenced memory, which means memory that is no longer reachable by any variables or data structures in your code.

  4. Garbage Collection: The garbage collector periodically scans the allocated memory to find and reclaim unreferenced memory. It marks the unused memory as available for reuse, making it eligible for allocation for new data. The exact algorithms and strategies used by garbage collectors can vary across JavaScript engines.

By automatically managing memory allocation and garbage collection, the JavaScript engine ensures efficient memory usage and prevents memory leaks, where unreleased memory keeps accumulating over time. However, there are cases where you can inadvertently introduce memory leaks by keeping unnecessary references to objects or data, preventing the garbage collector from reclaiming memory.


The memory heap and stack

In JavaScript, the memory allocation and storage of data are handled by two main data structures: the memory heap and the stack. Let's explore these data structures and their purposes:


Stack: Static memory allocation

The stack is used for static memory allocation, where the size of the data is known at compile time. It is a data structure that stores primitive values and references in JavaScript.

Primitive values such as strings, numbers, booleans, undefined, and null are stored directly on the stack. These values have a fixed size, and the JavaScript engine allocates a specific amount of memory for each value.


Additionally, references in JavaScript, which point to objects and functions, are also stored on the stack. These references store the memory address where the actual object or function is located in the memory heap.


Static memory allocation occurs before execution, as the engine determines the required memory for each value based on their known sizes.


The stack has a limit on the size of primitive values. The specific limits of these values and the stack size can vary depending on the browser or JavaScript engine being used.


Heap: Dynamic memory allocation

The memory heap is another data structure used by JavaScript engines to store dynamically-sized data. This includes objects, arrays, and other complex data structures that can grow or shrink during program execution.


Unlike the stack, the memory heap allows for dynamic memory allocation, as the size of these data structures can change at runtime.


When you create objects or arrays in JavaScript, the engine allocates memory for them on the memory heap. This memory allocation is dynamic because the size of the objects or arrays can vary based on their properties or elements.


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

Stack

Heap

Stores primitive values and references

Stores objects and functions

Size is known at compile time

Size is known at run time

Allocates a fixed amount of memory

Dynamically allocates memory

Used for static memory allocation

Used for dynamic memory allocation

Has a limit on the size of primitive values

No limit per object in terms of size

Here are the code examples along with the memory allocations:


Example 1:
const person = {
  name: 'John',
  age: 24,
};

JavaScript allocates memory for the person object in the heap. The values of name and age are primitive and stored in the stack.


Example 2:
const hobbies = ['hiking', 'reading'];

Arrays are objects, so JavaScript allocates memory for the hobbies array in the heap.


Example 3:
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

In this example, JavaScript allocates memory for the string 'John' and the number 24. When the value of name is changed to 'John Doe', JavaScript allocates memory for the new string. Similarly, when name.slice(0, 4) is called, JavaScript allocates memory for the substring 'John'.


It's important to note that in the case of primitive values, JavaScript creates new instances when their values are changed or manipulated, rather than modifying the original value in place.


References in JavaScript

In JavaScript, variables initially store their values in the stack. However, when a variable holds a non-primitive value (such as an object or function), the stack contains a reference to the object stored in the heap.


The heap is a memory area where objects and functions are stored. Unlike the stack, the memory in the heap is not ordered in any particular way. To keep track of objects in the heap, JavaScript uses references, which can be thought of as addresses. These references (addresses) are stored in the stack, and they point to the corresponding objects in the heap.



Let's look at an example:

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

In this example, a new object is created in the heap with the properties name and age. The variable person in the stack holds a reference to this object in the heap. Multiple variables can reference the same object, as shown in the following example:

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

const newPerson = person; // newPerson references the same object as person

In this case, both person and newPerson hold references to the same object in the heap.


References are a fundamental concept in JavaScript and play a crucial role in how objects are handled. Exploring references in depth goes beyond the scope of this article, but if you're interested in learning more, feel free to leave a comment or subscribe to the newsletter for further discussions.


Garbage collection

Garbage collection is the process of releasing memory that is no longer needed in order to free up resources. In JavaScript, the garbage collector is responsible for managing this process.


There are different algorithms used for garbage collection, and two commonly used ones are reference-counting garbage collection and the mark and sweep algorithm.


Reference-counting garbage collection:

The reference-counting garbage collection algorithm keeps track of how many references point to each object. When an object's reference count reaches zero, it means that there are no more references to that object, and it can be safely removed from memory.

Here's an example to illustrate this:

// Initial state
const person = { name: 'John' }; // Reference count of person: 1
let newPerson = person; // Reference count of person: 2// Removing references
newPerson = null; // Reference count of person: 1

// At this point, the object { name: 'John' } has a reference count of 1, so it is not garbage collected.

In the example above, the object { name: 'John' } initially has a reference count of 1. When newPerson is assigned to person, the reference count becomes 2. However, when newPerson is set to null, the reference count decreases to 1. Since there is still one reference pointing to the object, it is not garbage collected.


Mark and sweep algorithm:

The mark and sweep algorithm is a more advanced garbage collection algorithm. It works by traversing the object graph starting from known roots (global objects, stack variables, etc.) and marking all objects that are still reachable. Then, it sweeps through the memory, deallocating the memory occupied by the unmarked (unreachable) objects.



The mark and sweep algorithm can handle cases where there are cyclic references, where objects refer to each other in a circular manner, making them unreachable by reference-counting.


While reference-counting garbage collection is simpler, it has limitations in handling cyclic references. The mark and sweep algorithm provides a more robust approach to garbage collection.


In the example you provided with the lines representing references, the reference-counting garbage collection would remove the objects that have no references pointing to them, while the objects with references would remain in memory.


Garbage collection is an essential aspect of memory management in JavaScript, ensuring that memory is efficiently used and freeing up resources when they are no longer needed.

Because son and dad objects reference each other, the algorithm won't release the allocated memory. There's no way for us to access the two objects anymore.


Setting them to null won't make the reference-counting algorithm recognize that they can't be used anymore because both of them have incoming references.


Trade-offs

Automatic garbage collection in JavaScript provides convenience and allows developers to focus on building applications without manually managing memory. However, there are trade-offs associated with this approach:


Memory usage

JavaScript applications may consume more memory than necessary because the garbage collector cannot precisely determine when memory will no longer be needed. Even though objects are marked as garbage, the actual collection of memory is determined by the garbage collector itself. This means that memory may be held longer than necessary, potentially impacting overall memory usage.


If memory efficiency is a critical concern, lower-level languages that provide more control over memory management might be a better choice. However, working with such languages introduces other complexities and trade-offs.


Performance

Garbage collection algorithms run periodically to identify and clean up unused objects. The exact timing of garbage collection is not under developer control, which means there can be uncertainty regarding when it will occur. Collecting a large amount of garbage or performing frequent garbage collection operations can impact performance since it requires computational resources.


Fortunately, the impact of garbage collection on performance is typically negligible and goes unnoticed by both users and developers.


Memory leaks

Memory leaks occur when objects that are no longer needed are still being referenced, preventing their release from memory. Understanding how memory management works can help avoid common memory leaks.


One common cause of memory leaks is storing data in global variables. In JavaScript for the browser, if a variable is declared without var, const, or let, it becomes attached to the global window object. To avoid this, it is recommended to run code in strict mode.


If you intentionally use global variables, it's important to release the memory by assigning null to the variable:

window.users = null;

By understanding the underlying memory management principles and being mindful of potential memory leaks, developers can write more efficient and reliable code.


Global variables

One common type of memory leak is storing data in global variables. In JavaScript for the browser, if you accidentally omit the var, const, or let keywords when declaring a variable, it becomes attached to the window object.


For example:

users = getUsers();

To avoid this issue, it is recommended to run your code in strict mode. Strict mode enforces stricter rules for variable declarations and prevents accidental creation of global variables.

To enable strict mode, add the following line at the beginning of your JavaScript file or script tag:

"use strict";

By using strict mode, any variables that are not explicitly declared with var, const, or let will result in a reference error, preventing accidental global variable creation.


However, there may be cases where you intentionally use global variables. In such situations, it's important to release the memory associated with these variables once you no longer need the data they hold.


To release memory tied to a global variable, you can assign null to it:

window.users = null;

By assigning null, you remove the reference to the data held by the global variable, allowing the garbage collector to free up the memory associated with it.


Remember, proper management of global variables and freeing up memory when it's no longer needed can help prevent memory leaks and ensure efficient memory usage in your JavaScript applications.


Forgotten timers and callbacks

Another common cause of memory leaks is forgetting about timers and callbacks in JavaScript applications. This issue can lead to increased memory usage, especially in Single Page Applications (SPAs) where event listeners and callbacks are added 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);

In the above code, the function is executed every 2 seconds. However, if you no longer need the interval to run continuously, it's important to clear it to avoid unnecessary memory usage.


To clear the interval, use the clearInterval function:

clearInterval(intervalId);

Clearing the interval ensures that the objects referenced within the interval can be garbage collected when they are no longer needed. This is especially crucial in SPAs, as even when navigating away from the page, the interval may continue running in the background.


Forgotten callbacks:

const element = document.getElementById('button');
const onClick = () => alert('hi');
element.addEventListener('click', onClick);
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);

When adding event listeners, it's important to remove them once they are no longer needed. In the example above, the event listener for the 'click' event is added to the button element, but it's later removed and the element itself is removed from the DOM. This ensures that the event listener and the associated objects can be garbage collected properly.


Out of DOM reference:

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);
  });
}

If you store DOM elements in JavaScript, it's important to remove them from both the DOM and any associated data structures. In the example above, the element is removed from the DOM, but it's also necessary to remove it from the elements array to keep it in sync. Failing to remove the element from the array can result in a memory leak, as the element and its associated parent and children nodes may not be garbage collected.


By being mindful of timers, callbacks, and DOM references, you can prevent memory leaks and ensure efficient memory management in your JavaScript applications.

0 comments
bottom of page