Good day, friends!
In the vast majority of cases, as JavaScript developers, we don't need to worry about working with memory. The engine does it for us.
However, one day you will run into a problem called "memory leak", which can only be solved by knowing how memory is allocated in JavaScript.
In this article, I will explain how memory allocation and garbage collection work, and how to avoid some of the common problems associated with memory leaks.
Memory life cycle
When you create a variable or function, the JavaScript engine allocates memory for it and releases it when it is no longer needed.
Allocating memory is the process of reserving a specific space in memory, and freeing memory is freeing that space so that it can be used for other purposes.
Each time a variable or function is created, memory goes through the following stages:
- Memory allocation - the engine automatically allocates memory for the created object
- Memory usage - reading and writing data to memory is nothing more than writing and reading data from a variable
- Freeing memory - this step is also automatically performed by the engine. Once the memory is freed, it can be used for other purposes.
Heap and stack
The next question is: what does memory mean? Where is the data actually stored?
The engine has two such places: the heap and the stack. Heap and Stack are data structures that are used by the engine for different purposes.
Stack: static memory allocation
All data in the example is stored on the stack because it is a primitive.
A stack is a data structure used to store static data. Static data is data whose size is known to the engine at the compilation stage of the code. In JavaScript, such data are primitives (strings, numbers, booleans, undefined and null) and references that point to objects and functions.
Since the engine knows that the size of the data will not change, it allocates a fixed size of memory for each value. The process of allocating memory before executing your code is called static memory allocation. Since the engine allocates a fixed size of memory, there are certain limits on this size, which are highly browser dependent.
Heap: dynamic memory allocation
The heap is for storing objects and functions. Unlike the stack, the engine does not allocate a fixed size of memory for objects. Memory is allocated as needed. This memory allocation is called dynamic. Here is a small comparison table:
Stack | Heap |
---|---|
Primitive values โโand references | Objects and functions |
The size is known at compile time | The size is known at runtime |
Fixed memory allocated | Memory size for each object is not limited |
Examples of
Let's look at a couple of examples.
const person = {
name: "John",
age: 24,
};
The engine allocates memory for this object on the heap. However, property values โโare stored on the stack.
const hobbies = ["hiking", "reading"];
Arrays are objects, so they are stored on the heap
let name = "John";
const age = 24;
name = "John Doe";
const firstName = name.slice(0, 4);
Primitives are immutable. This means that instead of changing the original value, JavaScript creates a new one.
Links
All variables are stored on the stack. In the case of non-primitive values, the stack stores references to an object on the heap. Memory on the heap is disordered. This is why we need links on the stack. You can think of links as addresses and objects as houses at a specific address.
In the image above, we can see how the various values โโare stored. Note that person and newPerson point to the same object
Examples of
const person = {
name: "John",
age: 24,
};
This creates a new object on the heap and a reference to it on the stack
Garbage collection
As soon as the engine notices that a variable or function is no longer used, it frees the memory it was occupying.
In fact, the problem of freeing unused memory is unsolvable: there is no perfect algorithm for solving it.
In this article, we will look at two algorithms that offer the best solutions to date: reference counting garbage collection and mark and sweep.
Garbage collection through reference counting
Everything is simple here - objects to which no reference points are deleted from memory. Let's look at an example. Lines represent links.
Note that only the "hobbies" object remains on the heap, since only this object is referenced on the stack.
Cyclic links
The problem with this garbage collection method is the inability to define circular references. This is a situation where two or more objects point to each other but do not have xrefs. Those. these objects cannot be accessed from the outside.
const son = {
name: "John",
};
const dad = {
name: "Johnson",
};
son.dad = dad;
dad.son = son;
son = null;
dad = null;
Since the objects "son" and "dad" refer to each other, the reference counting algorithm will not be able to free memory. However, these objects are no longer available to external code.
Algorithm for tagging and cleaning
This algorithm solves the problem of circular references. Instead of counting the references that point to an object, it determines the accessibility of the object from the root object. The root object is the "window" object in the browser or the "global" object in Node.js.
The algorithm marks objects as unreachable and removes them. Thus, circular references are no longer a problem. In the above example, the objects "dad" and "son" are unreachable from the root object. They will be marked as trash and removed. The algorithm in question has been implemented in all modern browsers since 2012. The improvements made since then are about implementation and performance improvements, but not the core idea of โโthe algorithm.
Compromises
Automatic garbage collection allows us to focus on building applications and not waste time on memory management. However, everything comes at a price.
Memory usage
Given that it takes some time for algorithms to determine that memory is no longer being used, JavaScript applications tend to use more memory than they actually need.
Even though the objects are marked as garbage, the collector must decide when to collect them so as not to block the program flow. If you require your application to be as efficient as possible in terms of memory use, you are better off using a lower-level programming language. But keep in mind that such languages โโhave their own tradeoffs.
Performance
Garbage collection algorithms run periodically to clean up unused objects. The problem is that we, as developers, don't know exactly when this will happen. Large amounts of garbage collection or frequent garbage collection can affect performance as it requires a certain amount of processing power. However, this usually happens unnoticed by the user and developer.
Memory leaks
Let's take a quick look at the most common memory leak problems.
Global Variables
If you declare a variable without using one of the keywords (var, let, or const), the variable becomes a property of the global object.
users = getUsers();
Executing your code in strict mode avoids this.
Sometimes we declare global variables on purpose. In this case, in order to free the memory occupied by such a variable, you must assign it the value "null":
window.users = null;
Forgotten timers and callbacks
If you forget about timers and callbacks, your application's memory usage can increase dramatically. Be careful, especially when creating single page applications (SPA) where event handlers and callbacks are added dynamically.
Forgotten timers
const object = {};
const intervalId = setInterval(function () {
// , , ,
// ,
doSomething(object);
}, 2000);
The above code runs the function every 2 seconds. If you no longer need the timer, you must cancel it by:
clearInterval(intervalId);
This is especially important for SPA. Even if you go to another page where the timer is not in use, it will run in the background.
Forgotten callbacks
Suppose you register a handler for a button click that you later delete. In fact, this is no longer a problem, but it is still recommended to remove handlers that are no longer needed:
const element = document.getElementById("button");
const onClick = () => alert("hi");
element.addEventListener("click", onClick);
element.removeEventListener("click", onClick);
element.parentNode.removeChild(element);
Links outside the DOM
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));
});
}
If you remove any of these elements, you should also remove it from the array. Otherwise, such items cannot be removed by the garbage collector:
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);
});
}
I hope you found something interesting for yourself. Thank you for attention.