The Javascript Engine: Part Two

Call Stack and Memory Heap in V8

When working in Javascript, one doesn't always think too much about the call stack or memory heap directly as these are all managed by our engine, but these are critically important programming concepts that can help us write better code. In order to keep track of all of our variables, data, and so on we need the Memory Heap. This is used for the reading or writing of our data. All object types are stored on the heap, including our classes and functions. We also need something to run and keep track of each line of code. For this, we use the Call Stack. This lets us run things in the intended order, working in a First In Last Out (FILO) order. It will store our static data and some primitive types like int and string, and pointers to objects in our heap memory heap so we can access our variables.


//call stack + memory heap example
const number = 610; //allocate memory for number
const string = 'some text' // allocate memory for a string
const human = { // allocate memory for this object and its values
	first: 'Bob',
	second: 'Bobson'
}

function calculate() { //store function and variables in memory heap
	const total = 5 + 5;
	return subtractFive(total);
}

function subtractFive(num) {
	return num - 2;
}

calculate(); // put function into call stack


Garbage collection

Javascript is a garbage collected language, which means that when Javascript allocates memory in the heap, when we are finished calling a function and no longer need the data from this function, the javascript engine will automatically clear the memory. After a garbage collection pass, only the data we still need is kept. This helps developers by automatically managing memory, and can help prevent memory leaks. It does not make leaks impossible however. It can also give us a false impression that memory management or thinking about memory is not important.

The V8 engine uses the mark and sweep algorithm to run its garbage collection. This algorithm will go through every reference we have, mark it as active, and then sweep through and remove any unmarked memory. This is a very common algorithm in garbage collectors.

If we wanted to create a memory leak, we can do it like so:


let array = [];

for (let i = 5; i > 1; i++) {
	array.push(i -1)
}


This runs an infinite loop adding i-1 to the array, and as a result will fill our memory heap leading to the program to crash. The garbage collector cannot clean up this memory because it is still in use within our loop.

This is of course a silly demonstration that you will likely never encounter in real code, however, there are other ways that can easily lead to memory leaks. One example of this using global variables, which is fortunately not so common in modern javascript. A much more common way found today would be using 'Event listeners' in our code without cleaning them up when we are done with them. If a user then goes back and forth between pages with event listeners, memory will be taken up each time they enter the page, but it won't be cleaned up when they leave.

Another common memory leak in javascript is through using setInterval method. If we reference objects within our setInterval, the garbage collector will not clean up these objects as the function is still using them, unless we clear it with the clearInterval method.


//memory leak

//global variables
var a=1;
var b=2;
var c=3;		

// because these have a global scope (they are counted as VariableEnvironments), they will never be cleaned up.


//event listeners
var element = document.getElementbyId('button');
element.addEventListener('click', onClick);

// unless we run element.removeEventListener('click', onClick) it will never be cleared by our Garbage Collector


//setInterval
let intervalId = setInterval(myfunction, 1000) 

//unless we run clearInterval(intervalId) and null our intervalId variable, this will not be cleaned up by our Garbage Collector.


Javascript Runtime Environment

An important note is that the javascript code is executed in a single thread. This means that it has to operate in sequential order as it has only one call stack, one memory heap, and can only do one task at a time. If we want another program or another function to run, it has to wait until the previous one is finished. This makes it sound as if we're not able to actually run asynchronous code, but the reality is that behind the scenes our browsers each have their own version of a Javascript Runtime, providing us with what we call the Javascript Runtime Environment. This Environment consists of:

  • The JavaScript engine (containing out Call stack and Memory heap)
  • The browsers Web APIs
  • The browsers callback queue
  • The browsers event loop

So within our environment, all of these components are all working together.

Our browsers are all running their own javascript engine implementation. The browser also runs our Web API, usually written in a low-level language for speed, which listens to DOM events, sends http requests, and provides the javascript engine access to calls like fetch(), setTimeout(), our browsers session data, and so on. So with this environment our browsers actually give us the ability to achieve asynchronous code. Our browsers Web API runs separately from our javascript, which will then use the Event loops Callback queue to return to our engines call stack. Here is a very basic demonstration of this asynchronous event:



console.log('1');
setTimeout(() => { console.log('2'), 1000});
console.log('3');


In this demo, we can see that if this was done purely synchronously, we would end up with a sequential result of 1, 2, 3. Instead, our result will be 1, 3, 2, because we are using the Web API in the browser to execute our console log of 2 as a callback. Regardless of how long it takes for the engine to execute its second console log (value of '3'), it will always execute this call before the callback. This is because the callback is sent into our browsers Event loop, containing the callback queue, which is only sent to our call stack when it is empty and the file has been fully run through once.

This is a really cool web tool we can use to visualize this exact process in the javascript runtime. Knowing all this means we can pass any requests that may take a long time, or any DOM events straight to the browser which will then handle everything for us on the side and send it to the callback queue once the task is finished. So by using these tools we can run asynchronous code on our websites.

Difference between the Javascript Engine and Runtime

A popular analogy I am quite fond of which demonstrates the differences between the Javascript Engine and the Javascript Runtime is as follows:

The code we write, is like the music sheet. It is a series of notes.

The engine is like the musician, who can read the notes and understand it.

The runtime is like the whole package together, with the musician having access to the notes, and the instruments to allow him to play music.

So we can see how all three work in conjunction to allow us to create amazing sites and apps.

Javascript Runtime Environments are not limited to just web browsers however. Outside of the browser, another example of a very popular Javascript runtime is Node.js, which uses the Javascript V8 engine along with its own API completely different to the browsers Web API.

With both this post and the previous post, we should now have a solid foundational understanding of Javascript Engines, especially the V8 engine.