🧠 Part 1 - Memory Management in JavaScript: 10+ Data Structures & Garbage Collection Techniques | JS Deep Dive
Tags: javascript, programming, webdev, react
Published: 2024-06-03 16:53:48

- Memory management is a fundamental aspect of programming that varies significantly between languages.
- Low-level languages like C require manual memory management through functions like malloc() and free(), whereas high-level languages like JavaScript automate this process through garbage collection.

> This article delves into the intricacies of memory management in JavaScript, exploring its lifecycle, allocation strategies, and the role of garbage collection. 🌟

## Understanding the Memory Lifecycle 🧠
Every programming journey begins with understanding the memory lifecycle. Whether you're coding in C or JavaScript, the process is remarkably consistent:

- Allocate: Request the memory you need.
- Use: Utilize the allocated memory (read/write).
- Release: Free the memory once it's no longer required.

While the first and third steps are explicit in low-level languages, JavaScript handles the first and last steps implicitly, thanks to its automatic memory management features. 🛠️

## Allocation in JavaScript 🖥️
JavaScript elegantly manages memory allocation to simplify development. When you declare values, it automatically allocates memory:

```
const n = 123; // Allocates memory for a number
const s = "azerty"; // Allocates memory for a string

const o = {
  a: 1,
  b: null,
}; // Allocates memory for an object and its contents
const a = [1, null, "abra"]; // Allocates memory for an array and its elements

function f(a) {
  return a + 2; // Allocates a function
}
document.getElementById('myButton').addEventListener('click', () => {
  document.body.style.backgroundColor = 'blue';
}, false);
```

## Releasing Unneeded Memory 🗑️
Releasing memory when it's no longer needed is crucial. High-level languages like JavaScript employ garbage collection (GC) to automate this process. GC monitors memory allocation and decides when memory can be safely reclaimed. However, due to the complexity of determining exactly when memory is no longer needed, GC implementations approximate this process. 🕵️‍♀️

## Garbage Collection Explained 🧹
Garbage collection is a sophisticated technique used to manage memory. It works by identifying objects that are no longer accessible (i.e., not referenced by any live variables) and deallocating their memory.

> Two primary GC algorithms used are reference counting and mark-and-sweep.

### Reference Counting 📈
Reference counting tracks the number of references to an object. If an object's reference count drops to zero, it's considered safe to deallocate its memory. However, this method struggles with circular references, leading to memory leaks.

```
let primary = {
  key1: {
    key1_nested_key1: 2,
  },
};
// 2 objects are created. One is referenced by the other as one of its properties.
// The other is referenced by being assigned to the 'primary' variable.
// None can be garbage-collected.

let secondary = primary;
// The 'secondary' variable is the second thing that has a reference to the object.

primary = 1;
// Now, the object that was originally in 'primary' has a unique reference
// embodied by the 'secondary' variable.

let tertiary = secondary.key1;
// Reference to the 'key1' property of the object.
// This object now has 2 references: one as a property,
// the other as the 'tertiary' variable.

secondary = "Nikhil";
// The object that was originally in 'primary' has now zero
// references to it. It can be garbage-collected.
// However, its 'key1' property is still referenced by
// the 'tertiary' variable, so it cannot be freed.

tertiary = null;
// The 'key1' property of the object originally in primary
// has zero references to it. It can be garbage collected.
```

### Mark-and-Sweep 🪲
Mark-and-sweep is a more advanced Garbage Collection strategy. It starts from a set of root objects (e.g., global variables) and marks all objects reachable from these roots.
Afterwards, it sweeps away all unmarked objects, deallocating their memory.

> This method efficiently handles circular references, making it the preferred choice for modern JavaScript engines. 🚀

## Configuring Engine's Memory Model 🛠️

- JavaScript engines allow developers to configure memory settings, such as increasing the maximum heap size or exposing the garbage collector for debugging purposes.

- For instance, Node.js offers flags to adjust memory limits and enable garbage collector inspection:

```
node - max-old-space-size=4096 myScript.js
node - expose-gc - inspect myScript.js
```

## Advanced-Data Structures for Memory Management 🧩

- JavaScript introduces data structures like WeakMap and WeakSet, designed to aid in memory management by allowing objects to be garbage collected even if they're referenced within these structures.

- These structures hold weak references, meaning they don't prevent the garbage collector from collecting the referenced objects.

### WeakMap and WeakSet 🌐

- WeakMap and WeakSet both allow you to associate objects with other objects or track unique values without preventing those objects from being garbage collected.
- This feature is particularly useful for caching systems or tracking dependencies without holding onto unnecessary memory.

```
const weakMap = new WeakMap();
const key1 = {};
weakMap.set(key1, { key1 });
// Now `key1` cannot be garbage collected,
// because the value holds a reference to the key1,
// and the value is strongly held in the map!

```

### WeakRef and FinalizationRegistry 🔄

- WeakRef and FinalizationRegistry offer deeper insights into the garbage collection process.
- WeakRef allows you to hold a weak reference to an object, enabling it to be garbage collected while still accessing its value.
- FinalizationRegistry lets you register callbacks to run when an object is about to be garbage collected, facilitating cleanup operations.

```
const cache = new Map();
const registry = new FinalizationRegistry((value) => {
  cache.delete(value);
});

function cached(fetcher) {
  return async (key) => {
    let value = cache.get(key);
    if (value !== undefined) {
      return value.deref();
    }
    value = await fetcher(key);
    cache.set(key, new WeakRef(value));
    registry.register(value, key);
    return value;
  };
}
const getImage = cached(async (url) => fetch(url).then(res => res.blob()));
```