Memory Management In JavaScript

Memory Management In JavaScript

ยท

15 min read

Managing memory manually like we do in C and in some other languages is really painful but in JavaScript we don't have to do that manually that's why JavaScript is a garbage-collected language. In C we can use functions like malloc, free, calloc and realloc to allocate and de-allocate memory but this process is automatic in JavaScript. Knowing how memory management is done, lets the developer use the memory optimally and effectively, it also helps us finding the memory leakage so that we can optimize our code and don't run out of memory. In this article we're gonna look how the memory is managed in JavaScript and what algorithms are used for de-allocating the memory.

What is memory management?

Memory management is a crucial process which involves handling system's memory during the execution of the program. It involves allocation and de-allocation of memory as needed to store and manipulate data. Different programming languages have various approaches to manage the memory, in some languages memory management is automatic and in some memory management is manual.

What is memory life cycle?

The memory life cycle refers to the various stages that memory goes through during the execution of a program. Regardless of the programming language the memory cycle is always pretty much the same. This cycle includes Allocation, Initialization, Access and Modification, and finally De-allocation. The memory life cycle is essential for efficient resource utilization and preventing memory-related issues. Properly managing memory through these stages helps avoid problems like memory leaks (where memory is allocated but not de-allocated) or accessing invalid memory locations. Let's understand memory life cycle steps briefly.

  1. Allocation - When a variable or a data structure is created some memory needs to be allocated to store the data. During this stage, a block of memory of a certain size is allocated to that variable or data structure.

  2. Initialization - After the memory is allocated to the variable it may initialize the allocated memory with some value.

  3. Accessing and modifying - When the program executes the CPU access and modify the data that is stored in the memory as needed for further calculation, storage or some other task.

  4. De-allocation - When the program is finished or there is no longer need of the variable or the data structure then the corresponding memory is released so that it can be available for other use.

Till now we have covered what is memory management and its life cycle now let's jump to how these things work in JS. We'll try to cover all the important memory related concepts in JS that everyone should know.

Managing memory in JavaScript

In JavaScript there are 2 different data types first is primitive and the other is non-primitive. Primitive data type includes number, null, string, symbol, boolean, big int,undefined and non-primitive includes object literals, arrays, functions (because functions are indeed considered as object in JS).

Primitive Data TypeNon-Primitive Data Type
Primitive data types are stored inside stack.Non-primitive data types are stored inside heap
Fixed memory is allocated.Memory size is not fixed.
They are pre-defined types provided by programming language.These are implement in a way that they involves or build upon primitive data types.

The JavaScript engine stores the primitive data type in stack memory and non-primitive in heap memory. Let's understand stack and heap memory in detail.

  • Stack - Stack is a linear data structure that stores static data and uses FILO (First-In Last-Out) principle to manage itself. The size of stack is known during the compile time because it stores primitive data types and reference to objects and functions inside it. In JavaScript stack is generally refers to the "call stack" which is a part of memory that is used to manage the flow of execution in a program. When we create a variable (assuming it's a primitive type) it is allocated some memory and pushed in the stack to track its execution. Let's visualize it.

    Let's see the memory life cycle of this example (assuming that there are some calculation beneath the code and after the calculation is done the script will end.)

    1. When the JS code is being compiled by the JS engine then during that phase these variables are allocated memory (1st step of memory cycle completed).

    2. After the compilation is done and the script starts executing, these variables will be initialized with values. For example, the variable month will be initialized with 'December,' and likewise, the other variables will be initialized as well (2nd step of memory cycle completed).

    3. As per our assumptions, there are some calculations beneath our code. During the calculations, the memory blocks of these variables will be accessed and modified if needed (3rd step of memory cycle completed).

    4. The execution is finally completed all the stack is empty now which means that the memory is released (4th step of memory cycle completed).

  • Heap - Heap is another part of the memory that is used to store data, heap is used for dynamic memory allocation. It is less structured an unorganized as compared to stack and is used to store non-primitive data types because their size is not known during the compile time instead their size is determined during the runtime. If heap is not well structured and is not organized then how the JS engine manages to keep track of the non-primitive data types? Whenever we define a non-primitive data type then a pointer to that variable is pushed inside the stack so that JS engine can keep track of it.

Short summary of stack and heap:-

StackHeap
Managed automatically by the JavaScript engine, follows LIFO.We have to manage it manually in some languages but in JS it is automatic through garbage-collection.
Limited to simple data types (primitives like numbers, strings, boolean) and references to objects.Supports complex data structures (objects, arrays, functions) and instances of custom classes.
Size needs to be known at the time of function execution (compile-time).Size can vary, and it is determined dynamically, allowing for more flexibility.

We now have a clear picture of the memory cycle and the regions of memory that JS engine uses to store the data. But now the main question arises that how the last step of memory cycle that is de-allocating the memory works automatically in JS.

How JavaScript releases the memory?

Memory in JavaScript is automatically released through a process called garbage collection. It is a mechanism by which JavaScript engine identifies and frees up memory that is no longer in use. The JS engine uses various garbage collection algorithms and techniques to efficiently handle the memory de-allocation. Some common garbage collection techniques and algorithms are:-

  • Reference Counting with Cyclic Detection

  • Mark and sweep algorithm

  • Generational Garbage Collection

Now let's discuss these algorithms one by one.

  1. Reference Counting with Cyclic Detection - Reference counting with cyclic detection is a memory management technique that is used to automatically reclaim even in the presence of circular dependencies. To understand this better let's break it in 2 parts.

    • Reference Counting - The basic idea of this technique is that each object in the memory has a reference counter associated with it. Each time a new object establishes reference with that object then the counter of that object will increase by 1, so for example if 3 new objects establishes reference with that object then it's will increase by 3. When a reference is released then the reference count will decrease. If the reference counter reaches zero, then it indicates that it is no longer reachable and can safely de-allocated.

    • There is a major problem with reference counting and it is cyclic references. Cyclic references occurs when a group of objects references to each other. In such cases reference counting may not be sufficient to release the memory because the reference count of this object-reference-cycle never reaches zero.

    • Cyclic Detection - To handle the cyclic reference problem, reference counting with cyclic detection is used. This uses a mechanism to identify and break cycles. The program performs a cycle detection algorithm periodically to identify cycle of objects with non-zero reference count that are not reachable from the root of the object graph. Once the cycle is identified, the program needs to break the cycle to make the reference count drop to zero. This breaking is done by nullifying the references within the cycle. Breaking the cycle allows the reference count of the objects within the cycle to reach zero immediately, resulting in proper memory de-allocation.

      Note - The common algorithm used for cycle detection is "tracing" or "mark and sweep".

    • How this algorithm works? To understand this, assume that we have 3 objects A, B and C. They all are referenced to each other and A is also reachable from root. The algorithm will start and A will be marked as true because it's reachable from root then the references of A that is B and C will be traversed and they'll get marked too (because they're referenced with A). Now A's references are marked then the algorithm will check B's references while checking B's references it'll detect that A object is already marked this traversal will continue till it'll mark all the referenced objects. The object cycle is identified during the mark phase, if an object has already been marked it means that there is an object cycle. Once the cycle is detected the algorithm can take corrective action, such as breaking one or more references within the cycle.

  2. Mark and Sweep Algorithm - The mark and sweep is a garbage collection technique that is used to identify and reclaim memory occupied by objects that are no longer reachable in a program. This algorithm operates in two phases:- 1. Mark phase 2. Sweep phase.

    • Mark Phase - Every object that we create has a flag of a single bit that is reserved for the garbage collection purpose only and initially this bit is marked with 0 (false) when the object is created. The 0 bit represents that the object hasn't been visited yet and 1 bit represents that the object is reachable. In the mark phase each object that can be reached from the root is marked with 1 (true) and this process is done by performing a depth-first-search (DFS) on the object graph starting from the root.

      What's this root? "Root" here refers to the starting point for the garbage collection process typically it's the global variable, local variable, static variables,etc... The garbage collector starts from the root and follows the references and marks each object's bit with true ensuring that only the objects reachable from the root are considered live and retained in memory.
      Note that the location of "root" depends on the programming language but in JavaScript, roots for garbage collection are typically associated with the global object and local variables within the current call stack.

    • Sweep phase - The sweep phase start after the mark phase, the garbage collector scans the entire memory heap for objects that are not marked with 1 (true), those objects that are not marked are considered as "dead" or "unreachable" objects, which means that they're no longer being used in the program. So, for every object that was not marked, the garbage collector free up the space that was occupied by that object this process deletes the object from the heap. After the unreachable objects are de-allocated, all the other objects are set to 0 (false) because the algorithm will run again (if required), and again the reachable objects will mark as true and this process will continue.

  3. Generational Garbage Collection - The main idea of Generational Garbage Collection (GC) technique is that it divides heap memory in two generations, young generation and old generation. A observation is used to optimize this garbage collection technique and that's, most objects die young meaning that the newly allocated objects tend to be short-lived, while older objects tends to live longer. When an object is created it is placed in the young generation and as time passes, if the object that is placed in the young generation survives a certain point, it is then moved to old generation. This is based on the assumption that most objects will not live long enough to be promoted to the Old Generation.

    • The young generation is collected more frequently then the old generation because it is smaller and contains fewer object, so the cost of collecting it is lower as compared to old generation.

    • The old generation is larger and it contains more object than young generation so it is collected less frequently.

    • Common algorithms used in both young and old generations are "Mark and sweep" and "Mark-sweep-compact".

    • I have already covered the mark and sweep algorithm so now I'm gonna explain only the compact phase. In the compact phase, the algorithm compacts the memory space by moving live objects together and freeing up the fragmented memory (Non-contiguous memory chunks). Live objects are shifted to one end of the heap, leaving the other end as a contiguous block of free memory. This helps in reducing memory fragmentation and ensures efficient memory utilization.

Memory leak in JavaScript?

A memory leak is a situation when the memory is allocated to the objects or any data structure during execution but the de-allocation of memory fails. There are several types of memory leaks:-

  • Unused Memory: This occurs when memory is allocated but never used.

  • Lost Memory: This happens when a pointer to a block of memory is lost, causing the program to lose track of the memory and thus unable to free it.

  • Circular References: These occur when two or more objects reference each other, preventing them from being garbage collected even though they're no longer needed.

Reasons of memory leak in JavaScript and how to prevent them :-

  1. Unreleased Event Listeners:-

    • Cause -> Not removing the attached event listeners from the elements when they're removed from the DOM can lead to memory leak.

    • Prevention -> Always remove event listeners using removeEventListener when elements are no longer needed.

          // Example 
        const button = document.getElementById('button'); // Assume that we have a button element
        function welcomeUser () {
            alert('Welcome back! ๐ŸŽ‰');
        };
      
        button.addEventListener('click', welcomeUser);
        // Prevention :- remove the event listener when its no longer needed
        button.removeEventListener('click', welcomeUser);
      
  2. Uncleared Timers and Intervals:-

    • Cause -> Timers and Intervals that are not removed also causes memory leak and it's important to clear them.

    • Prevention -> Use clearTimeout and clearInterval to remove timers and intervals when they are no longer necessary.

        // Example
        const timer = setTimeout(() => {
            console.log('This is timer');
        }, 1000);
      
        // Prevention:- Clear the timer when it's no longer needed
        clearTimeout(timer)
      
  3. Global Variables:-

    • Cause -> Variables declared without let, or const become global and may persist longer than needed.

    • Prevention -> Always declare the variable with let and constant with const keyword. Avoid creating unnecessary global variables.

        // Example
        m1 = 'Memory may leak'; // Never do this. This will get added in the global object.
        // Prevention:- Don't declare global variables
        let m2 = 'No memory leak';
      
  4. Circular References:-

    • Cause -> Objects referencing to each other creates a object cycle and may cause memory leak.

    • Prevention -> Break circular reference or use weak reference. Set the references to null when done.

        // Example
        const obj1 = {
            username: 'John Doe',
            age: 20
        }
        const obj2 = {
            hobbies: ['Coding', 'Blogging'],
            designation: 'Software Engineer'
        }
        obj1.reference = obj2;
        obj2.reference = obj1;
      
        // Prevention:- Break the object cycle when it's no longer needed.
        obj1.reference = null;
        obj2.reference = null;
      
  5. DOM Reference After Removal:-

    • Cause -> Holding references to DOM elements after removal prevents their memory from being released.

    • Prevention -> Set references to DOM elements to null or remove them after they are no longer in use.

        // Example
        const button = document.getElementById('btn');
        // some other logic
        // removing the element
        document.body.removeChild(button);
      
        // Prevention: Set the reference to null
        element = null;
      

There could be more scenarios of memory leaks but I have covered the most common cases.'

โญ Summary

Let's summarize what we have learnt so far:-

  • Memory management is a process which involves handling system's memory this includes allocation and de-allocation of memory.

  • There are 4 steps of memory life cycle. 1. Allocation, 2. Initialization, 3. Accessing and modifying and 4. De-allocation

  • In JavaScript there are 2 different data types. 1. Primitive. 2. Non-Primitive. The JavaScript engine stores the data in stack and heap. The Primitive data types are stored inside stack and they're allocated fixed amount of memory. On the other hand Non-Primitive data types are stored inside heap memory because their size is not known during the compile time.

  • Stack is organized and follows LIFO (Last in first out) principle, while heap is not organized so to keep the track of the data, pointers are used. Each object has a pointer that is stored inside the stack and it points to that particular object in the heap.

  • Freeing up the memory is the most crucial part. But in JavaScript its automatic because it's a garbage collected language.

  • Some algorithms and techniques used for memory de-allocation in JavaScript are:- 1. Reference Counting with Cyclic Detection, 2. Mark and sweep algorithm, 3. Generational Garbage Collection

  • Reference counting with cyclic detection is a garbage collection method that tracks and adjusts the number of references to objects, preventing memory leaks in the presence of cyclic references by identifying and breaking such cycles.

  • The Mark and Sweep algorithm is a garbage collection method consisting of two phases. In the Mark phase, it marks reachable objects from the root, and in the Sweep phase, it frees up memory occupied by unmarked (unreachable) objects, preventing memory leaks and optimizing memory usage.

  • Generational Garbage Collection optimizes memory management by dividing the heap into Young and Old Generations, prioritizing more frequent collection for short-lived objects in the Young Generation and less frequent collection for long-lived objects in the Old Generation.

  • A memory leak is a situation when the memory is allocated to the objects but the de-allocation of memory fails.

  • Some memory leaks scenarios in JavaScript are:- 1. Unreleased timers, 2. Unreleased events, 3. Declaration of global variables, 4. Object reference cycle, 5. DOM reference after removal and more...

Thank you so much for reading my article ๐Ÿฅณ, hope you have learned something new today. If you have any questions or have any feedback for me to improve my article, please feel free to use the comment box. See ya in my next article ๐Ÿ™‹๐Ÿปโ€โ™‚๏ธ.

ย