Understanding Memory Management in Javascript
A God-like skills for all kinds of developers
Prologue
Once upon a time in the faraway land of C++ lived two evil twins called malloc() and alloc(). The evil twins controlled every variable and function that wanted to enter certain parts of the city called programs.
Slowly and steadily their influence and evil eye increased and started causing issues like Memory Leaks and Garbage data in almost all programs. The citizens had no choice but to relocate to other realms such as Java, Javascript, and others.
In this article, we will explore how these evils can be avoided in our very own mystic realm of Javascript.
Introduction
Continuing onto our ultimate quest for World Domination using Javascript, we're here seeking the ultimate Super Saiyan power, i.e., effective Memory Management in Javascript.
Memory is a limited resource (just like time, we think we have unlimited time, but in fact, it is very limited). Thus, it is important to spend memory wisely and save as much memory as possible in our programs.
This ultimately helps us avoid issues like Memory Leaks, increased resource consumption, etc.
What is Memory Management
Memory management refers to the process of allocating, utilizing, and releasing memory resources when no longer needed. It ensures efficient utilization of memory and prevents memory leaks or excessive memory usage.
JS allocates memory for variables and objects dynamically, meaning that it assigns memory to them at runtime rather than at compile time. When a variable or object is created, JS determines the amount of memory it needs and assigns it accordingly. This process is known as dynamic memory allocation.
Memory Life Cycle
In JS, memory management involves the management of two critical components: the Stack and the Heap.
The Stack
The Stack is a region of memory used for storing static data such as function calls, local variables, and function scope. It operates on the Last-In-First-Out (LIFO) principle. This memory is managed by the JS engine and developers have no control over it.
The Heap
The Heap is a region of memory used for dynamic memory allocation. It is where objects, closures, and large data structures reside.
Here, the memory allocation and deallocation in the Heap are more complex.
Memory Allocation
In JavaScript, variables can be assigned primitive values (like numbers and booleans) or reference values (like objects and arrays). When a variable is assigned a primitive value, it is stored directly in the Stack. However, when a variable is assigned a reference value, the reference is stored in the Stack, while the actual object resides in the Heap.
Consider the below-given example:-
let x = 42; // Primitive value stored in the Stack
let obj = { name: 'John Wick' }; // Reference stored in the Stack, object stored in the Heap
Garbage Collection
Garbage Collection is the process of automatically reclaiming memory that is no longer in use by the program. JavaScript employs a garbage collector to free up memory occupied by objects that are no longer reachable.
Garbage collection involves identifying objects that are no longer needed and releasing their memory. JavaScript’s garbage collector works by traversing the object graph and marking reachable objects. Unreachable objects are then removed in the next step using algorithms such as Reference Counting, Mark and Sweep algorithm and, Generational Garbage Collection.
Reference Counting
The reference counting garbage collection algorithm works by keeping track of the number of references to an object. When an object no longer has any references, it is considered eligible for garbage collection. While this algorithm is simple and efficient, it has some limitations. For example, it cannot detect circular references.
Mark and Sweep Algorithm
Mark and Sweep algorithm works in two phases:
Marking Phase - During the Mark phase, the garbage collector traverses the object graph, starting from the root objects (global object, execution contexts, etc.) and traversing the object graph while marking (or tagging) reachable objects.
Any object that is not marked during this phase is considered unreachable and is eligible for garbage collection.
Sweeping Phase - In the Sweep phase, the garbage collector sweeps out all the non-reachable objects to free up the space. This free space can later be used for new memory allocation.
In this phase, one may see performance issues if there are a large number of objects to be swept.
This algorithm is more sophisticated than reference counting and can handle circular references, however, it is also slower and requires more memory.
Generational Garbage Collection
Modern JS engines also employ a technique called Generational Garbage Collection. This approach divides objects into different generations based on their age. Frequently accessed objects are moved to younger generations, while less frequently used objects are promoted to older generations. This optimization reduces the garbage collection overhead and improves performance.
Memory Leaks and Performance Issues
Memory leaks can occur in JavaScript when objects are unintentionally kept in memory, even though they are no longer needed.
Causes of Memory Leaks:
Memory leaks can happen due to circular references, unclosed resources, global variables, unremoved event listeners, and improper use of closures. These issues can lead to excessive memory consumption and degrade application performance.
Identifying and Fixing Memory Leaks
To identify memory leaks, tools like Chrome DevTools Memory tab and memory profilers can be used. To fix memory leaks, ensure that resources are properly released, event listeners are removed when no longer needed, and variables are appropriately scoped.
Best Practices for Memory Management
To optimize memory management in JavaScript, follow these best practices:
Avoid Global Variables:
Global variables remain in memory throughout the application’s lifecycle. Minimize the use of global variables and prefer encapsulating variables within functions or modules.
Properly Dispose of Event Listeners:
When adding event listeners, ensure they are properly removed when no longer needed. Otherwise, they can create memory leaks by holding references to objects that should be garbage collected.
Reducing object size:
Large objects can consume a lot of memory, which can be especially problematic in large-scale applications. To reduce object size, consider using primitive types instead of objects, using arrays instead of objects, and minimizing the use of nested objects. Additionally, consider using immutable objects where possible, as they can help reduce memory usage and improve performance.
Use Object Pooling:
For frequently created and destroyed objects, consider implementing object pooling. Object pooling reuses existing objects instead of creating new ones, reducing memory allocation and garbage collection overhead. Let’s see an optimized code example-
Advanced Memory Management Techniques
JavaScript provides advanced memory management techniques to handle specific scenarios.
WeakMap and WeakSet
WeakMap and WeakSet are special data structures in JavaScript that allow objects to be weakly referenced. They can be useful for scenarios where objects need to be garbage collected when they are no longer referenced elsewhere.
Memory Management in Frameworks
Frameworks like React and Angular handle memory management for components. Understanding the memory management strategies employed by these frameworks can help optimize the usage of components and prevent memory leaks.
Conclusion and Next Steps
In this article, we’ve explored the intricate world of Memory Management and Garbage Collection in JavaScript. Understanding how memory is managed and released is essential for writing efficient and performant code. By following best practices, being aware of memory leaks, and leveraging advanced memory management techniques, you can optimize memory usage in your JavaScript applications. Keep exploring, keep learning, and happy coding!