visit
Garbage collection is simulating a computer with an infinite amount of memory. --
In JavaScript (similarly to many other languages) there are two main types of memory Stack and Heap. Both are managed by the , both are for storing runtime data. The difference lays in speed and size. Heap is bigger and slower, Stack is smaller and faster.
How does the engine know which one to use? The rule of thumbs is: if the engine is not sure about the size it uses Heap. If the engine can calculate the size beforehand, it uses Stack.
All the primitives like number
, boolean
, string
, Symbol
, BigInt
, null
and undefined
always go to the Stack. Also, references are stored there, we'll talk about references in a minute.
What's left will end up in the Heap. This includes arbitrary objects and functions.
Memory allocation in the Stack (aka static memory allocation) happening right before the code (next lexical scope) execution. References are stored in the Stack, thus they are allocated before the code is executed. Hence if we declare the variable it will be available even before the actual declaration in the code. Although value will be undefined
because it doesn't have value to point to yet...
console.log(yolo); // undefined
var yolo = "hello!";
Variables declared with let
, var
, const
are hoisted, although let
and const
won't return undefined
.
This is the gist of how references work. The short description is a variable name, the shelf number is a memory address. The address is stored in the variable, which is stored in the Stack. And the actual object on the shelf is an object stored in the Heap, referenced by the variable...
Every time we use the assign (=) operator we are not assigning the value... We are creating a pointer to the memory where the value is stored. Your variable storing the address, that pointing to the memory where the actual value is stored.
I think the language we use matters. Therefore I think the word "assign" and operator =
is evil misleading and creates cognitive confusion and unnecessary simplification. I think a huge amount of bugs came from such confusion.
I'd personally prefer to be more explicit about what is happening and suggest using a term like "pointing" or "referencing" instead of "assigning" and operator like ->
instead of =
.
let answer = 42;
As we figured before we are not setting value we are pointing to it... Pretty straightforward so far, let's make it a bit more complicated...
let answer = 42;
let true_answer = answer;
answer = 43;
console.log(answer); // 43
console.log(true_answer); // 42
Same principle here. First both answer
and trueAnswer
point to the same address where value 42
is stored. Once we do answer = 43
we change not the value, but memory where we pointing...
Primitives are immutable. It kinda becomes obvious and almost redundant to mention if we talk it through. If we try to change 42
(e.g. add 1
to it), we will simply get another number, which is not 42
...we won't change 42
(42
will still exist)... Hence it is immutable. Nor we can extend it. E.g. 42.value = 22
won't work, although it will if 42
would be an object...
Let's do another example with primitives... null
and undefined
are primitives. What does that mean? They act like all primitives...
const null1 = null;
const null2 = null;
console.log(null1 === null2); // true
let undefined1;
let undefined2;
console.log(undefined1 === undefined2); // true
Now we see why both values are strictly equal, pointing to the same value.
console.log(typeof null); // object
It is not true, null
is not an object. It is that can't and won't be fixed...
const a = true;
const b = false;
const c = true;
const d = false;
const e = true;
Everything looks very familiar.
This is where the =
operator will reveal its full evilness 😈.
const catzilla = { name: "Catzilla", breed: "Bengal Cat" };
const peanut = catzilla;
peanut.name = "Peanut";
console.log(catzilla); // { name: "Peanut", breed: "Bengal Cat" }
console.log(peanut); // { name: "Peanut", breed: "Bengal Cat" }
Remember that the =
points to the data. We are just routing pointers here.
const catzilla = { name: "Catzilla", breed: "Bengal Cat" };
const peanut = { ...catzilla };
peanut.name = "Peanut";
console.log(catzilla); // { name: "Catzilla", breed: "Bengal Cat" }
console.log(peanut); // { name: "Peanut", breed: "Bengal Cat" }
With the help of ...
() we managed to clone whatever catzilla
was pointing to in the new address and made peanut
point to it. This is not the original intention, how this operator should be used. But (as it usually happens with JavaScript) this side-effect was warmly accepted by the JavaScript community as a way to perform shallow cloning.
const breed = {
name: "Bengal Cat",
origin: "United States",
color: { pattern: "spotted", name: "brown" },
};
const catzilla = { name: "Catzilla", breed: breed };
const peanut = { ...catzilla };
peanut.name = "Peanut";
peanut.breed.color.name = "marble";
console.log(catzilla);
/*
{
name: "Catzilla",
breed: {
name: "Bengal Cat",
origin: "United States,
color: {
pattern: "spotted",
name: "marble"
}
}
}
*/
console.log(peanut);
/*
{
name: "Peanut",
breed: {
name: "Bengal Cat",
origin: "United States,
color: {
pattern: "spotted",
name: "marble"
}
}
}
*/
// ...
const peanut = JSON.parse(JSON.stringify(catzilla));
// ...
console.log({} === {}); // false
const value1 = {};
const value2 = {};
console.log(value1 === value2); // false
To understand it completely we need to understand how the equals ==
and the strictly equal ===
operators work, unfortunately, it is not very trivial. However, to prevent this article from bloating, let's just say that comparison happening by actual value in the variable. As we know now it is an address of the object, rather than value. Because we are pointing to two different objects, located by two different addresses. Values are not equal...
Principle lays in finding unreachable objects...
The unreachable object is any object that can't be reached via traversal through references from the so-called root. In browser-world root is represented by the window
object (aka Global Scope).
Just a side-note 📝, that all global variables in JavaScript are not hanging in the air, they are rather attached with references to the window
object...
Unlike the Mark and Sweep, this one will try to find unreferenced objects instead of unreachable... This algorithm does not try to determine whether the object is still needed (in the previous example reachable from the root). Instead, it only checks if anything references the object.
function catdog() {
let cat = {};
let dog = {};
cat.dog = dog;
dog.cat = cat;
return "hello";
}
catdog();
The above code (if used with the current algorithm) creates a memory leak. Because memory allocated for variables cat
and dog
will never be collected, even though it never used in outer scope...🐱🐶
Nowadays usage of global variables is a mauvais ton (bad practice). If happens, it is usually accidental. This problem can be easily caught by the linter 👮. Or prevented from happening by adding use strict
at the beginning of the file.
window
).
setInterval(() => {}, 1000);
const memoryLeak = {
counter: 0,
massiveData: new Array(100).join('I am your memory leak!');
};
setInterval(() => memoryLeak.counter++, 1000);
The memoryLeak
object will never be released even though we might not need the whole object anymore.
// ...
const timerId = setInterval(() => memoryLeak.counter++, 1000);
// do stuff
clearInterval(timerId);
Another classical one. If you are working with something like React or Angular, there's no reason to worry. Nevertheless, it is an interesting way to lose some memory 🧠... It is a camouflage version of the global variable memory leak. And it happens even nowadays pretty often, usually in-between the script
tags.
We reference arbitrary DOM elements in the code (e.g. by calling document.getElementById('i-will-leak')
)
Even though we delete the element from the DOM it still hangs in the lexical scope or global scope (e.g. by calling document.body.removeChild(document.getElementById('i-will-leak'))
)
<script>
var memoryLeak = document.getElementById('i-will-leak');
document.body.removeChild(memoryLeak);
</script>
The memoryLeak
will never be garbage-collected, the removeChild
here is very misleading, it seems like it will remove the element from everywhere, but it does it only for the DOM tree.
<script>
function doStuff() {
var memoryLeak = document.getElementById('i-will-leak');
document.body.removeChild(memoryLeak);
}();
</script>