JavaScript Engine, JIT compiler, Stack, Heap, Memory, Primitives, References, and Garbage Collection

Max Shahdoost
13 min readApr 20, 2024

--

JavaScript Fundamentals

In this article, I want to dive deep into a couple of fundamental topics of JavaScript language and its engines such as interpretation, JIT compilation, referencing, stack, heap, memory management, and garbage collection.

Before getting started with those subjects let’s first review a couple of important concepts which knowing them would help us digest a lot of important advanced knowledge about JavaScript and even other languages.

Programming Language Types

We have two common types of programming languages, they are either a compiled or an interpreted language but this is tricky and different when it comes to JavaScript. JavaScript uses a mixed way of handling code between interpretation and compilation we will talk about this in a few minutes so please bear with me to learn more about compilation and interpretation now!

Compilation vs Interpretation

The process of translating high-level code written by programmers into an executable set of binary instructions that a computer CPU can understand is called compilation. It is a relatively complicated and long process. The result is a binary code.

The process of translating high-level code written by programmers into an executable set of binary instructions that a computer CPU can understand line by line while executing is called interpretation. It is relatively faster because the amount of compiled code is smaller and yet because of the added time it takes to compile, the execution time is relatively slower in comparison to the compiled languages.

Compiler vs Interpreter languages simplified

JavaScript Engines

JavaScript is a scripting programming language responsible for converting the codes we write in a text script format into a binary executable set of instructions for a computer CPU.

JavaScript has multiple engines developed by giant tech companies and integrated into modern browsers, they are:

1- V8: developed by Google.
2- SpiderMonkey: developed by Mozilla Firefox.

3- JavaScriptCore: developed by Apple for Safari.

4- Rhino: developed and written in Java by Mozilla Foundation.

5- Chakra: developed by Microsoft and is used in the Edge.

6- JerryScript: a JavaScript engine for IoT ( Internet of Things).

JavaScript is not an interpreted or compiled language, it is a mix of interpretation and compilation because of the Just-In-Time or JIT compiler it uses under the hood.

JavaScript Just-in-Time (JIT) compilers work by dynamically compiling JavaScript code at runtime to native machine code, allowing it to execute faster than interpreted code. The general idea is to compile code that is executed frequently or takes a long time to run.

Here are the steps of how the JavaScript engine works using the JIT compiler:

  1. Parsing: As a new piece of code enters the JavaScript Engine, the first step is parsing the code into a data structure called the Abstract Syntax Tree (AST).
  2. Profiling: Now the JIT compiler identifies the parts of the code that are executed most frequently, which are the parts that should be optimized.
  3. Optimization: The JIT compiler then applies various optimization techniques to the frequently executed code, such as inlining functions, eliminating redundant code, and generating specialized machine code.
  4. Code generation: Once the optimization process is complete, the JIT compiler generates binary code that can be executed directly by a CPU.
  5. Execution: Finally, the optimized binary code is executed by a CPU quickly and efficiently.
JIT Compiler in JavaScript simplified illustration

All this parsing, compilation, and optimization happens in some special threads inside the engine that we can not access from our code. So, separate from the main thread running into the call stack executing our code. Now different engines implement in slightly different ways, but in a nutshell, this is what Modern Just-in-time compilation looks like for JavaScript.

JavaScript Data Structures:

We’ve covered a lot so far, we discussed the compiled and interpreted languages, the details of JavaScript language under the hood, why it is called JIT compiled, and how it works. Now it is time to jump to our main topic here, for those of you who are currently experienced with JavaScript and for those who are beginners willing to learn I am going to bring up a refresher about JavaScript data structure and data types.

As you all may know, JavaScript is a dynamic language with dynamic types, it is a weakly typed language because it allows type conversion (coercion) when operations are executed. It allows data types to be converted if they are mismatched instead of throwing errors. When we are allocating data to variables we don’t explicitly tell the language types of data we are going to use for that piece of variable, it also can be changed during the coding and execution time depending on how it is declared.

Generally, we have two different data structures in JavaScript:

1- Primitives (Value) types.
2- References (Object) types.

Primitive types are:

1- Null

2- Undefined
3- Boolean
4- String
5- Number
6- Symbol
7- BigInt

Reference types are anything other than the primitive types for instance objects, functions, and arrays and they are also called objects. We call them reference types because they are possibly referenced by an identifier in the memory and when we change them we are mutating the memory reference.

You can read more about data structures in JavaScript here if you are curious to learn more:

Having said that the JavaScript engine’s memory management has two components and they’re called Heap and Stack. Let’s define them first and see what is their responsibilities and tasks.

1- Stack (static memory allocator):

Stack is a data structure that the JavaScript engine uses under the hood to store Static Data. Static data is data where the JavaScript engine knows the size at compile time. In JavaScript, this includes primitive values and references that are identifiers to objects in the memory. In stack, there will be stack frames whose size is fixed and it is not possible to change it.

2- Heap (dynamic memory allocator):

The Heap is a different memory space for storing data where JavaScript stores objects in the computer memory. Unlike the stack, the engine doesn’t allocate a fixed amount of memory for these objects. Instead, more space will be allocated as needed.

Important Note (Call Stack vs Stack):

Call Stack is different than Stack or aKa Thread Stack, call stack and thread stack are both data structures but their duty and the way they work are different.

The call stack in JavaScript is a mechanism used to keep track of the function calls in a program. Whenever a function is called in JavaScript, a new frame is created and pushed onto the call stack. This frame contains the function’s arguments and local variables.

The call stack is a Last In, First Out (LIFO) data structure, which means that the most recently added frame is the first one to be removed. When a function finishes executing, its frame is popped off the top of the stack, and program execution resumes from the previous point on the stack.

Important Note (Pass by Value and Pass by Reference):

When we pass primitive values in functions or assign them to other variables, the JavaScript engine copies the value and passes it to the new variable that’s why we call them pass-by-value, however, when we assign a previously created object to a new variable, it is passed by reference and it is not going to copy the reference because it will require a huge amount of memory and it is not efficient!

Here is a good example to understand it better:

// First Example
const num1 = 1;
const num2 = num1; // Passed by value (Copied)
const num3 = 1;

console.log(num1 === num2); // true
console.log(num3 === num1); // true
console.log(num3 === num2); // true

// Second Example
const obj1 = {age: 30};
const obj2 = {age: 30 };

console.log(obj1 === obj2); // false (Pointing to different memory allocation)

// Third Example
const obj3 = {name: "max"};
const obj4 = obj3; // Passed by reference (Pointing to the same memory allocation)

console.log(obj3 === obj4); // true

Here is a code snippet and respective diagram and I am going to explain how it is allocated in the memory and how it is going to execute:

JavaScript V8 runtime Stack and Heap memory allocation

As we said, JavaScript executes and compiles the code line-by-line since it is a mix of Interpreted and Compiled language, when the main thread reaches the plane constant since the plane is an object, the heap will allocate a memory space to it and the plane will in the stack referenced to that memory allocation in the heap however the name = “max” is a primitive string type so it is a fixed data size and will be allocated in memory by stack itself. This will go on and when we reassign the plane to the new plane you can see that it will not create a new memory allocation, it will reference to the same memory space it previously created.

This is very important to know because while we are working in JavaScipt with objects or more modern frameworks and libraries like Next.js and React.js we need to be aware of the mutability and references since we will be working a lot with states, props, rendering optimizations and data comparison. After all, in React everything is compared with Object.is and this checks the reference in the memory. Keep in mind that when we want to change the state we need to create a new reference in the memory heap to force the observer of React or any other library/framework or tool to be aware of the changes, we need to pay attention to immutability of reference values.

type Props = {
plane: {
name: string;
age: number;
}
}

function ShowPlane({plane}: Props){

return (
<div>
<h1>{plane.name}</h1>
<p>{plane.age}<p>
</div>
);
}

For example, in the above component in React, if we pass the plane object to this ShowPlane component from a parent, every time the parent re-renders a new function execution happens and a new plane object will be created as a result so the plane object memory allocation in the heap will be changed and the ShowPlane will be re-rendered regardless of a completely same component and props in terms of data values, that’s why we will memorize them in React 18 and below versions.

Cloning (Deep Copy vs Shallow Copy):

In JavaScript, there are two ways to copy objects, shallow copy and deep copy. Shallow copying creates a new object with references to the same memory locations as the original object, while deep copying creates a new object with new memory locations for all of its properties and nested objects or arrays.

Methods like map, slice, filter, spread operator, and Object. assign etc will create a shallow copy of the original object, and methods like JSON.parse(JSON.stringy()), and structureClone will create a deep copy in the memory from the original object.

If you are confused about how methods like the map will create a shallow copy you can read this article:

These methods, if the original object is deeply nested with other objects, will create a shallow copy just like the rest operator so keep it in mind to prevent making errors there!

How JavaScript Works under the hood in a simplified way!

Memory Lifecycle:

Regardless of the programming language, the memory life cycle is pretty much always the same:

  1. Allocate the memory you need.
  2. Use the allocated memory (read, write).
  3. Release the allocated memory when it is not needed anymore.
Memory Lifecycle in every programming language

Memory Leak

It is important to keep in mind that memory is a finite resource, and in JavaScript, memory is stored in two places: the call stack and the memory heap. Given that we have limited access to these resources, it is crucial to write efficient code that can prevent issues like stack overflow or memory leaks and to effectively manage memory usage.

A memory leak occurs in JavaScript when the program continues to allocate memory without releasing it, leading to a reduction in the available memory of the system, which may eventually lead to the program crashing.

Memory Leak in a view/context change in JavaScript

Here are some common causes of memory leaks in JavaScript and how to avoid them:

  1. Forgotten event listeners: When an event listener is attached to an element, the listener function remains in memory until it is explicitly removed. If an element with an attached event listener is removed from the DOM, but the listener is not removed, it can cause a memory leak. To avoid this, always remove event listeners when they are no longer needed.
  2. Closures: Closures are a powerful feature of JavaScript, but they can also cause memory leaks. When a function creates a closure, any variables in the outer function that are used in the inner function will remain in memory until the closure is released. To avoid this, avoid creating unnecessary closures and make sure to release closures when they are no longer needed.
  3. Large data structures: Large data structures, such as arrays or objects, can consume a lot of memory if they are not properly managed. To avoid this, make sure to release any references to large data structures when they are no longer needed.
  4. Forgotten timers or callbacks: Having a setTimeout or a setInterval referencing some object in the callback is the most common way of preventing the object from being garbage collected. If we set the recurring timer in our code the reference to the object from the timer’s callback will stay active for as long as the callback is invocable.

Stack Overflow:

A “stack overflow” occurs in JavaScript when the call stack exceeds its maximum size. The call stack is a data structure used by JavaScript to keep track of function calls. Each time a function is called, a new entry is added to the top of the call stack. When a function completes, its entry is removed from the top of the call stack.

If a function calls itself (a recursive function), or if a chain of function calls becomes too long, the call stack can overflow. This can happen when there is an infinite loop or when a function calls itself too many times.

Stack Overflow in JavaScript
Call Stack Overflow in JavaScript

To prevent a stack overflow in JavaScript, you can:

  1. Avoid infinite loops: Make sure that your code doesn’t get stuck in a loop that never ends.
  2. Use tail recursion: If you need to use recursion, use tail recursion, which is a technique that allows the JavaScript engine to optimize the code and prevent stack overflow.
  3. Increase the stack size: Some JavaScript engines allow you to increase the maximum size of the call stack. However, this is not recommended as it can lead to other performance issues.
  4. Refactor your code: If you are experiencing a stack overflow, it may be a sign that your code needs to be refactored. Consider breaking up large functions into smaller ones, optimizing your code, and removing unnecessary recursive calls.

Garbage Collection:

Garbage collection in JavaScript is the automatic process of freeing up memory that is no longer being used by an application. JavaScript has a built-in garbage collector that manages memory automatically, so developers don’t have to worry about managing memory manually like other programming languages such as C and C++.

It uses different algorithms to manage memory, including mark-and-sweep, reference counting, and generational collection. The most common algorithm used in modern browsers is the mark-and-sweep algorithm.

Mark-and-sweep algorithm works by starting with a set of root objects, such as global variables or objects in the current execution stack. The garbage collector then traverses the object graph, marking objects that are reachable from the roots. Any objects that are not marked as reachable are considered garbage and can be removed.

JavaScript mark-and-sweep garbage collection algorithm

You can read more in-depth about JavaScript memory management here:

Conclusion:

Huuh! It was a long story, wasn’t it?

I hope that I have helped you to know more in-depth about the JavaScript engine and how it works behind the scenes, remember all we discussed here was a simplified and big picture of what is happening behind the scenes and there is defiantly a lot more to it if we go deeper however I think the above knowledge is beyond enough for people who are using JavaScript to craft an awesome and scalable softwares using JavaScript unless you want to go more about creating a runtime for JavaScript like Bun.js then you probably need to dig deeper in the topics.

Don’t forget to comment, like, and give me feedback if this was helpful for you, and have a happy time coding!

Resources:

--

--

Max Shahdoost

A highly passionate and motivated Frontend Engineer with a good taste for re-usability, security, and developer experience.