The Lifecycle of Memory in JavaScript

Alexandra Langton
The Startup
Published in
8 min readOct 30, 2020

Regardless of the language you use to write code, your program needs to allocate and access memory in order to store variables. In many high-level languages memory management is carried out for you, which makes for a simpler coding experience. Even if you’re using a high-level language it is still a good idea to have an understanding of the memory management process, and its potential pitfalls.

So, what do we mean by memory management? Low-level languages like C include memory management commands such as malloc( ) and free( ) which allow developers to explicitly manage memory allocation and deallocation. No such commands exist in JavaScript however. So then, how does this happen?

When JavaScript code is executed, the process of actually making the code run is managed by the runtime environment, which in the Google Chrome browser or Node.js is Chrome’s V8 engine.

As V8 compiles the JavaScript code to machine code (a low-level language the computer can interpret directly), it performs optimizations on the code and accounts for memory allocation. It also separately manages memory deallocation, through an automated process known as ‘garbage collection’.

In order to understand how much memory your program is using at any given time, you need to understand how it is being deallocated. When a variable is no longer needed in your program but is still present in memory, this is called a ‘memory leak’. With enough memory leaks, your program could in theory exceed the available memory and crash. In low level languages where you must manage your own memory, this occurs whenever you forget to free your memory. In JavaScript the garbage collector evaluates and removes unneeded variables from memory for you, and whilst this is more secure than relying on the developer to deallocate memory, there are still some circumstances in JavaScript where the garbage collector can fail and memory leaks can occur.

Memory Lifecycle

The memory lifecycle has three steps, common across most programming languages:

A diagram showing the three stages of memory management

The first and third steps are implicit in JavaScript.

1. Memory Allocation

JavaScript will automatically allocate memory when values are initially declared. This can happen in a variety of ways.

2. Using Variables

This stage of the memory lifecycle occurs when a variable that has been previously allocated memory is used by a program; for example its value is read or rewritten, the variable is passed to a function, etc.

3. Releasing Memory

In JavaScript and other high level languages, the process of removing unused variables from memory is called ‘garbage collection’. A ‘garbage collector’ monitors memory allocation and determines whether variables are still needed, and if not, frees up that piece of memory. There is no one algorithm or combination of algorithms which can predict with complete accuracy which variables are ready for garbage collection, and as such the question of when to release memory is considered ‘undecidable’.

Since there is no infallible method for determining when memory should be released, occasionally variables which are no longer needed by a program are not detected by the garbage collection algorithms and remain in memory (memory leaks).

Let’s look at how garbage collectors evaluate which objects can be removed from memory. There are two primary garbage collection algorithms:

Reference-counting Algorithm

This algorithm is the more naive of the two algorithms. In the reference-counting algorithm, an object is evaluated as ‘garbage’ and ready for collection if no other parts of the code reference it, either implicitly or explicitly. Some older browsers such as Internet Explorer 6 and 7 relied on this approach, and some garbage collectors still use this in conjunction with the mark and sweep approach.

This algorithm has one large drawback: circular references are not picked up by this method. If two variables reference each other but are not required in any other part of the code, this algorithm would not pick them up, as they are referenced and by the standards of this algorithm thus ‘needed’.

Mark and Sweep Algorithm

This more widely-used algorithm counts a variable as ready for collection if it is not connected to the global object. In the ‘marking’ part of this algorithm, the garbage collector visits all elements connected to the known root of a program (e.g. the DOM or the global object), and marks these elements as ‘reachable’, or ‘live’. It then recursively visits and ‘marks’ all elements connected to those live elements. This approach cuts out the problem of circular references; if an element is not connected to the global object, it will be not be marked as live, regardless of if it is referenced by other non-live elements.

In the ‘sweep’ phase the garbage collector clears the heap memory of all unmarked objects. This newly freed memory is then added to a list of available memory, and will be reallocated as new variables are created.

A diagram visualizing the Mark and Sweep garbage collection algorithm.
The Mark and Sweep Approach

Historically while garbage collection was taking place all other processes were paused (known as a ‘stop-the-world’ approach), which means slow garbage collection can have a very real impact on performance. For example, in older browsers you can very clearly see video stutters (known as ‘jank’) where garbage collection delays loading video footage. The below video shows a side-by-side comparison of the same game demo being played in two different Chrome versions. The impact caused by an older garbage collection version delaying frame loading is surprisingly large!

Source: https://v8.dev/blog/jank-busters

More modern garbage collectors try to minimize rendering delays and latency in a variety of ways. To take V8’s garbage collector Orinoco as an example, there are three main ways the program works to do so.

Parallel Collection

This is still a stop-the-world approach, however instead of only the main JavaScript thread carrying out the garbage collection process an additional number of helper threads carry out a similar amount of work at the same time, so the pause time is divided by the number of threads in play (plus some overhead time to synchronize these). This is the easiest of the three approaches since the main JavaScript thread is paused, and no new variables are being written into memory whilst the garbage collection runs.

Incremental Collection

In this approach all garbage collection is carried out by the main thread, but in intermittent stages. This way latency is reduced, even though the overall processing time is no shorter than with a traditional stop-the-world method. This is more complicated than a parallel collection, as between each incremental process memory can change, invalidating previous work carried out.

Concurrent Collection

Here the main thread operates JavaScript code uninterrupted and all garbage collection is carried out by helper threads in the background. This leaves the main thread completely free to run JavaScript but is the most challenging approach: at any point new variables can be created which invalidates the work done so far, and there can be races between the main and helper threads to access the same objects.

Despite these continual improvements in garbage collecting programs, memory leaks can still occur in JavaScript. There are four main ways these can take place:

Accidental global variables

If you assign a value to a variable that has not been declared, JavaScript will automatically designate this a global variable, and attach it to the global object. Global variables are by definition not available for garbage collection due to this attachment to the root element.

This type of memory leak can be avoided by being mindful of declaring global variables, or being sure to define all locally scoped variables. Using strict mode will also ensure that any undeclared variables are picked up in your code.

Timers

Let’s look at the setInterval( ) and setTimeout( ) methods. As long as they are active (which in the case of setInterval( ) can be until the program has completed running), the callbacks within them and the functions themselves cannot be marked as ready for garbage collection, regardless of whether variables referenced inside their callback functions are removed from scope.

In the above example, if we remove node and someExternalReference from scope, these will still point to each other, and so would not be picked up by the reference counting algorithm. This used to be a common source of memory leaks when this algorithm was still heavily used to free memory.

Whilst the introduction of the mark and sweep approach to garbage collection takes care of the circular reference issue, it remains best practise to cancel any attached event listeners before removing an element from your program.

Out of DOM references

Out of DOM references refer to nodes which have been removed from the DOM, but are still referenced in the JavaScript. For example, if I store a reference to a particular element in my code, but then remove the element from the DOM, as there is still a reference to it in the JavaScript code this cannot be removed from memory.

Bear in mind that memory leaks of this type also affect the parent elements. For example, if you have a reference to a particular <li> element saved in your code and then attempt to remove the entire list, not only will the referenced <li> element be kept in memory but also the entire list. This happens as the DOM is doubly linked; all parents contain references to their children and vice versa. So if any element is attached to the global object, the entire tree of nodes will be prevented from being deallocated in memory.

Closure

There is a particular scenario with which closures can result in memory leaks, which can be a little confusing, but is worth being aware of.

Consider this example:

As a quick reminder, a closure is the combination of a function with references to its lexical environment. Every time replaceMyVar runs, a new uselessMethod closure is created. This shares a lexical environment with uselessFunc, which references previousVar. Once a variable is used by a closure, it is kept in the lexical environment of all closures which share the same scope. In this example, since uselessFunc references previousVar, this variable is also bound to the uselessMethod closure since they share the same scope.

Although uselessFunc is never called (the clue is in the name here!), this reference pointing to previousVar binds it to uselessMethod. So in effect, we end up with a chain of uselessMethods referencing the previous myVar (which contained a uselessMethod, which referenced the previous myVar… and so on) from every time replaceMyVar is called. This keeps the previousVar objects ‘active’ in memory, and so prevents them from being eligible for garbage collection. So every time the replaceMyVar function runs another very large string is allocated to memory and never deallocated, contributing to a significant memory leak. The easy way to reduce the impact of this would be to assign previousVar the value of null between lines 16 & 17, which would keep the closure in uselessMethod’s scope, but with a much smaller value attached to it.

I hope that brief overview was helpful in understanding the basics of memory management in JavaScript! If you want to learn more, the below articles were helpful to me in building my understanding of the subject:

--

--

Alexandra Langton
The Startup

Alexandra lives in Brooklyn, where she spends most of her time writing code with her cat.