Most of the time, you can probably get by fine not knowing anything about memory management as a JavaScript developer. Afterall【毕竟】, 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【垃圾收集GC】 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【阶段】:
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.
“Objects“ in the context of memory management doesn’t only include JS objects but also functions and function scopes.
The memory heap【堆】 and stack【栈】
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.
Heaps and stacks 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【原始值】.
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.
To get an overview, here are the features of the two storages compared side by side:
Stack | Heap |
---|---|
Primitive values and references | Objects and functions |
Size is known at compile time | Size is known at run time |
Allocates a fixed amount of memory | No limit per object |
Examples
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, which is why they are stored in the stack.
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.【你可以把引用地址和对象在堆中如房屋属于这些地址。】
Remember that JavaScript stores objects and functions in the heap. Primitive values and references are stored in the stack.
In this picture, we can observe how different values are stored. Note how person
and newPerson
both point to the same object.
Examples
const person = {
name: 'John',
age: 24,
};
This creates a new object in the heap and a reference to it in the stack.
**
Garbage collection【垃圾回收GC】
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.
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.
Cycles
The problem with this algorithm is that it doesn’t consider cyclic references【循环引用】. This happens when one or more objects reference each other, but they can’t be accessed【访问】 through code anymore.
let son = {
name: 'John',
};
let dad = {
name: 'Johnson',
}
son.dad = dad;
dad.son = son;
son = null;
dad = null;
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.
Mark-and-sweep algorithm【标记与清扫算法】
The mark-and-sweep algorithm has a solution【解决】 to cyclic dependencies【循环依赖】. Instead of simply counting the references to a given object, it detects【查出】 if they are reachable from the root object【从根对象】.
The root in the browser is the window
object, while in NodeJS this is global
.
The algorithm marks the objects that aren’t reachable as garbage【垃圾】, and sweeps (collects) them afterward【清洁工收集之后】. Root objects will never be collected.
This way, cyclic dependencies are not a problem anymore. In the example from before, neither the dad
nor the son
object can be reached from the root. Thus【因此】, both of them will be marked as garbage and collected.
Since 2012, this algorithm is implemented in all modern browsers【应用在现代浏览器】. Improvements have only been made to performance【性能】 and implementation【实现】, but not to the algorithm’s core idea itself.
Trade-offs【权衡】
Automatic garbage collection allows us to focus on building applications instead of losing time with memory management. However, there are some tradeoffs【权衡】 that we need to be aware of【意识】.
Memory usage【内存使用】
Given that the algorithms can’t know when exactly memory won’t be needed anymore, JavaScript applications may use more memory than they actually need.
Even though objects are marked as garbage, it’s up to the garbage collector to decide when and if the allocated memory will be collected.
If you need your application to be as memory efficient【有效率的】 as possible, you’re better off with a lower-level language. But keep in mind that this comes with its own set of trade-offs.
Performance【性能】
The algorithms that collect garbage for us usually run periodically【周期性】 to clean unused objects.
The issue with this is that we, the developers, don’t know when exactly【精确的】 this will happen. Collecting a lot of garbage or collecting garbage frequently【频繁地】 might impact【影响】 performance since it needs a certain amount of computation power to do so.
However, the impact usually goes unnoticeable【不明显】 to the user or the developer.
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.
In JavaScript for the browser, if you leave out the var
, const
, or let
, the variable will be attached 【附件到】to the window
object.
users = getUsers();
Avoid this by running your code in strict mode.【避免这种情况通过运行您的代码在严格模式下。】
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
.
window.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【DOM的引用】
This memory leak is similar to the previous ones: It occurs【重现】 when storing DOM elements in JavaScript.【这种内存泄漏是类似于之前的:它发生在当存储在JavaScript DOM元素。】
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**.