You can think of computer memory as a large number of flip flops you can access by unique identifier. Each of them can store one bit. 8 bits form one byte. 16 (or sometimes 32) bytes form one word. Words are organized into pages. In V8 for example one page stores 8MB. Page also stores overhead in the form of metadata containing marked bitmap. For each word there is one bit reserved in bitmap.
This memory allocation happens right before the code is executed.
When we define a variable of primitive type in code compiler calculates memory consumption based on it’s type. Then the required memory is allocated in the Call Stack. Primitive types in JavaScript are numbers, strings, booleans, undefined
and null
. Call Stack is also a place where references pointing to objects and functions we create are stored. That’s possible because at compile time compiler knows the required size needed for storing reference.
Call Stack is a mechanism used to keep track of function invocations. It basically determines where we are when it comes to code execution. When you call a function it will be added on top of the Stack. If this function calls another one, that function will also be added on top of the Stack above the previous one. When function finishes its execution it will be removed from Stack.
Here is the visualization of how Call Stack would look like at different steps if you run this code:
const formatDate = (date, locale = "en-US") => {
const formattedDate = date.toLocaleString(locale);
return formattedDate;
};
console.log(formatDate(currDate));
When the variable we define is not primitive, e.g. object, array or function, the amount of memory is unknown at compile time and allocation is happening in the Heap. For this type of variables the engine allocates memory as needed at the moment. It’s worth noting that variable itself, the reference pointing to object, will still be stored on Call Stack. It means that variable only points to the object in Heap.
When the variable becomes unneeded the memory should be realeased. In JavaScript garbage collector is responsible for freeing memory. The big question here is to understand which memory is unused. There are several algorithms used most often to detect unused variables and release the allocated memory. Most of them rely on objects references to tell if memory they occupy can be freed or not. The object is considered live if it is referenced by root object or another active object.
In a nutshel it maintains a table with objects and number of references to them. In there are no references to object it becomes a candidate for clean up. Conceptually the algorithm is simple to understand. But it has a major flaw: if there is a cycle, i.e. several variables referencing each other, they reference counter will never go to zero.
const connectFriends = () => {
var user1 = {};
var user2 = {};
user1.friend = user2;
user2.friend = user1; // cyclic reference
};
connectFriends();
This is rather naive approach rarely used in browsers nowadays.
This approach is utilized by modern browsers.
Mark
In order to detect unused memory it traverses variables starting from the roots. Roots are global variables referenced in the code, they are live by definition. Think of window
in JavaScript.
While traversing the algorithm marks roots and children accessible from roots as active. When it’s done anything that left unmarked is considered a garbage and can be disposed.
To understand marking we can refer to tri-color marking abstraction. It uses colors to represent states of the objects in heap with the following meaning:
Sweep After previous stage is complete we need to free unused memory and return it to OS. This happens during Sweep phase and when it’s complete previously occupied space is added to free lists in memory page.
Compact This is a stage during which JavaScript engine optimizes usage of space and moves objects so memory pages become less fragmented. Feed out pages are released back to OS.
The approach described above solves the issue with cyclical references, as long as object is not accessible from the root it can be deleted.
It’s worth noting that garbage collection process requires the main thread to pause. For this reason multiple optimizations are typically introduced to reduce the time and number of cycles needed for garbage collection. In practice modern browsers utilize incremental marking and lazy sweeping techniques to speed up garbage collection cycles.
Heap memory is typically divided into multiple segments each serving different purpose in order to minimize the area of garbage collection and speed up further cycles.
Young Generation is a name of new objects allocated in New space. This space is typically small and is designed to be scavanged very fast.
New space is divided into two parts: ToSpace and FromSpace. Allocations of memory for new variables is happening in ToSpace. There is an allocation pointer which is incremented whenever there is a need to reserve space. Once the pointer reaches the end of New space JavaScript engine starts scavanging process. All the accessible variables are copied into FromSpace or promoted to Old space. Then it removes dead objects and swaps the spaces back. So further allocation continues to happen in the ToSpace.
Old space contains most objects if they have pointers from other objects. Such objects are called Old Generation and get promoted to this space after surviving garbage collection in New space. Running garbage collection in this space is expensive so it isn’t run as often as in New space. This space will be garbage collected during mark-sweep and mark-compact phases which are much less frequent.
Large object space contains objects larger than the size limits of other spaces. This space is never compacted by garbage collector.
Code space indludes JIT compiler instructions.
It’s a situation when the memory consumption grows overtime and never drops. Further iterations of garbage collector free less and less memory. The allocation pointer moves further and further without memory cleanup until Maximum stack size exceeded
exception is thrown.
Causes can be different, but the most common are: