DEV Community

Nik
Nik

Posted on

JavaScript memory management 101: Strong and Weak refs, FinalizationRegistry

When we assign something to the variable it can be either primitive (value) type or object (reference) type.

This article gives you an overview of the types, the difference between strong and weak ref, and how to track the object garbage-collection (GC).

Primitive and reference types

Reference and value types

Value and reference types are different. The most notable differences are: comparison, storage, and how engines manage these data.

📝 Comparison. Primitive types are compared by its value, reference types are compared by reference:

1 === 1 // true
{} === {} // false
Enter fullscreen mode Exit fullscreen mode

📝 Storage: value types are stored in the stack, reference types are stored in the heap. The value itself is just a pointer to the place in the heap, where it's stored.

// obj is a reference to the `{}`
const obj = {};

// a stores the number 123 in stack
const a = 123;
Enter fullscreen mode Exit fullscreen mode

📝 When the variable is stored in the stack, it gets cleared as soon as the execution context ends:

// When we call foo, we put the function context to the callstack
foo();

function foo() {
  // We add fooVariable to the stack and foo2 as well
  let fooVariable = 123;
  let foo2;

  function bar() {
    // Put barVariable to the stack
    let barVariable = 1234;

    // Here we copy the value
    foo2 = barVariable;

    // We get out from `bar` function, which means `barVariable` will be disposed
  }
  // Put `bar` function context to the callstack
  bar();

  // We get out from `foo` fn, so `fooVariable` and `foo2` will be disposed
}
Enter fullscreen mode Exit fullscreen mode

Note: we have 2 object types declarations in this example, can you find them?

Answer
We declared 2 functions: bar and foo. They are object types

📝 As reference types are stored in the heap, engines should "understand" when they can garbage collect the object and clear unused data, otherwise, you would get a memory leak. To do so, engines use Garbage-collector (GC).

The simplest GC implementation is the reference counter:
Illustration of the simplest GC
As you can see, we don't clear the object as soon as we get out of the function in comparison with value types. It's stored all the time we have any pointers to it.

Modern engines use complex GCs, which have lots of optimizations, such as generations, etc. (let me know if you're interested in it, we can cover it in the future 😊)

All the variables we were using so far are strong refs. We use them most of the time:

// Strong reference to array
const array = []; 

// Strong reference to object
const obj = {};

// Strong reference to map
const map = new Map();
Enter fullscreen mode Exit fullscreen mode

In opposite to strong refs, many languages allow developers to use weak refs.

Weak refs

📝 Weak refs are garbage-collectible references to objects. In other words, they don't prevent the object from being GCed.

WeakRefs are relatively new to the JS ecosystem. They don't prevent the stored value from being GCed:
Weak ref GC

📝 Weak references are used to prevent objects from being kept in the memory when there are no other strong references and you don't want this ref to prevent it.

Some examples which I saw before:

  1. If you want to control the execution order for promises. One of the most popular ways to do it is to execute them in declaration order: e.g. "stack promises". To prevent possible memory leaks, you can create the list of WeakRef<Promise>, and if any of the promises would be GCed, you just ignore the entry in your list.

  2. If you have a Map, which keys are objects and you don't want this map to keep objects in memory. As an example, such maps can be used to keep additional data associated with the object itself, like metadata. In this case, you can use WeakMap

  3. Temporal caches: we can have a cache, which has a limit for maximum entries inside the cache (e.g. LRU). Another use case is to create a cache that keeps the list of WeakRefs. Such a cache would keep the data as long as we have either a link for it or up to the moment when GC would remove unused entries.

JavaScript allows us to use 3 types of Weak* structures:
📝 1. WeakRef is an object that keeps a weak reference to another object without preventing it from being GCed.
📝 2. WeakMap is a map but the keys are weak references. Primitive types as keys are prohibited in WeakMaps. It covers the 2nd use case from the examples above (additional data to existing objects, like meta-data).
📝 3. WeakSet is the set of WeakRefs to the objects.

Important note: do not perform tests with WeakRefs in dev tools. Your objects won't be GCed in it due to DevTools specific behaviour. Instead, use jsbin, codesandbox, runkit or perform tests in Node.js. Localhost is okay when you open files using http:// protocol, as file:// also has specific behaviour.

Let's test some base WeakRef behaviour. How we can find that the object was GCed? To perform the test we need:

  1. Lose/remove all strong references to the object
  2. Allocate a lot of memory and remove strong refs to these objects too
  3. Make macrotasks pauses to increase chances for the GC
  4. Test WeakRef.

That's how we can allocate a lot of memory and make pauses
// Creates lots of objects to consume additional memory, and lost references to them, to speed up GC call
async function createLotsOfGarbageAndPause() {
  for (let i = 0; i < 1000; i++) {
    const a = new Array(100000);
  }
  await tick();
}
// Creates a separate Macro Task, so that it increases chances to have GC performed
function tick() {
  return new Promise((resolve) => {
    setTimeout(resolve, 0);
  });
}
Enter fullscreen mode Exit fullscreen mode

Now let's prepare the test code:

async function createLotsOfGarbageAndPause() { for (let i = 0; i < 1000; i++) { const a = new Array(100000); } await tick(); } function tick() { return new Promise((resolve) => { setTimeout(resolve, 0); }); } console.log("WeakRef example started"); let one = new WeakRef({ field: "this object usually will be presented in console, cause GC wouldn't be called" }); // This one may be GCed in really rare cases console.log( "To access the object inside WeakRef in js you have to call deref:", one.deref() ); let two = new WeakRef({ field: "However, once we allocate a lot of memory, we may have the object GCed" }); for (let i = 0; i < 10; i++) { await createLotsOfGarbageAndPause(); } // This one can be either defined or GCed console.log( "Instead of previous one this weakRef has more chances to be GCed", two.deref() == null ? 'The object is GCed' : 'The object still exists, try to re-run example' ); console.log("WeakRef example completed"); // !!!!!! SETUP LATEST NODE VERSION before running the code snippet !!!!

If Runkit didn't show the example
console.log("WeakRef example started");
let one = new WeakRef({
  field:
    "this object usually will be presented in console, cause GC wouldn't be called"
});

// This one may be GCed in really rare cases
console.log(
  "To access the object inside WeakRef in js you have to call deref:",
  one.deref()
);

let two = new WeakRef({
  field: "However, once we allocate a lot of memory, we may have the object GCed"
});
for (let i = 0; i < 10; i++) {
  await createLotsOfGarbageAndPause();
}
// This one can be either defined or GCed
console.log(
  "Instead of previous one this weakRef has more chances to be GCed",
  two.deref() == null ? 'The object is GCed' : 'The object still exists, try to re-run example'
);
console.log("WeakRef example completed");
Enter fullscreen mode Exit fullscreen mode

As you can see, when GC is executed we lose access to the Object with WeakRef. However, we may still have 2 questions:

Why do we have separate data types for WeakMaps and WeakSets?

Let's create a Map with objects as keys:

const map = new Map(); const a = {}; const b = {}; const weakRef = new WeakRef(b); map.set(a, 10); map.set(weakRef, 20); console.log('Obj a key', map.get(a)); console.log('Obj b key', map.get(b)); console.log('Obj weakRef key', map.get(weakRef)); // !!!!!! SETUP LATEST NODE VERSION before running the code snippet !!!!

When you save a weakRef as a key, you refer to the WeakRef object itself instead of the real one (b = {}). This means, that to save the weak ref as a key in a map you need to use a special type:

async function createLotsOfGarbageAndPause() { for (let i = 0; i < 1000; i++) { const a = new Array(100000); } await tick(); } function tick() { return new Promise((resolve) => { setTimeout(resolve, 0); }); } console.log("WeakMap example started"); let one = new WeakRef({ field: "This object doesn't have a strong reference to itself" }); let map = new WeakMap(); map.set(one.deref(), 10); for (let i = 0; i < 10; i++) { await createLotsOfGarbageAndPause(); } // This one can be either defined or GCed console.log( "Check if object exists", one.deref() ); if (one.deref() == null) { console.log('The object was GCed, and therefore it means WeakMap did not prevent it from being garbage-collected'); } console.log("WeakMap example completed"); // !!!!!! SETUP LATEST NODE VERSION before running the code snippet !!!! // !!!!!! Code takes some time to run, as we allocate a lot of memory !!!!

If Runkit didn't show the example
console.log("WeakMap example started");
let one = new WeakRef({
  field:
    "This object doesn't have a strong reference to itself"
});

let map = new WeakMap();
map.set(one.deref(), 10);

for (let i = 0; i < 10; i++) {
  await createLotsOfGarbageAndPause();
}
// This one can be either defined or GCed
console.log(
  "Check if object exists",
  one.deref()
);

if (one.deref() == null) {
  console.log('The object was GCed, and therefore it means WeakMap did not prevent it from being garbage-collected');
}

console.log("WeakMap example completed");
Enter fullscreen mode Exit fullscreen mode

📝 The regular Map doesn't allow you to use WeakRefs as keys, you can either refer to the WeakRef object itself or have a strong reference as a key.
📝 WeakMap creates a WeakRef for objects which we use as keys and doesn't prevent them to be GCed.
📝 WeakMap doesn't have .entries or .keys methods as regular Map, and therefore we cannot iterate through WeakMap elements. WeakMap has following methods: .has, .get, .set.

How to get the exact moment when the object gets GCed?

JavaScript provides FinalizationRegistry.

📝 FinalizationRegistry provides a way to set up a callback that gets called when an object in this registry is GCed.

The code for the test will be almost the same but we will include FinalizationRegistry to log the moment when the object gets GCed:

async function createLotsOfGarbageAndPause() { for (let i = 0; i < 1000; i++) { const a = new Array(100000); } await tick(); } function tick() { return new Promise((resolve) => { setTimeout(resolve, 0); }); } console.log("FinalizationRegistry example started"); // Create a strong ref to the object let strongRef = { field: "Once this object is garbage collected (finalized), we will see it in the console" }; const weakRef = new WeakRef(strongRef); const time = performance.now(); let collected = false; // Create a FinalizationRegistry (observer); const registry = new FinalizationRegistry((value) => { // weakRef.deref should be undefined here console.log( 'The object with field: ' + value + ' was just finalized. TimeConsumed: ' + ( performance.now() - time ).toFixed() + '. WeakRef: ' + weakRef.deref() ); collected = true; }); // Register our object registry.register(strongRef, strongRef.field); // remove all StrongRefs from the object strongRef = null; // Wait! while (collected !== true) { await createLotsOfGarbageAndPause(); } console.log("FinalizationRegistry example completed"); // !!!!!! SETUP LATEST NODE VERSION before running the code snippet !!!!

If Runkit didn't show the example
console.log("FinalizationRegistry example started");
// Create a strong ref to the object
let strongRef = {
field:
"Once this object is garbage collected (finalized), we will see it in the console"
};

const weakRef = new WeakRef(strongRef);
const time = performance.now();
let collected = false;
// Create a FinalizationRegistry (observer);
const registry = new FinalizationRegistry((value) => {
// weakRef.deref should be undefined here
console.log(
'The object with field: ' + value + ' was just finalized. TimeConsumed: ' + (
performance.now() - time
).toFixed() + '. WeakRef: ' + weakRef.deref()
);
collected = true;
});

// Register our object
registry.register(strongRef, strongRef.field);

// remove all StrongRefs from the object
strongRef = null;

// Wait!
while (collected !== true) {
await createLotsOfGarbageAndPause();
}
console.log("FinalizationRegistry example completed");


📝 One of the most popular use cases for FinalizationRegistry is memory logging. If your application works with big data chunks, you can use FinalizationRegistry to make reports to understand whether your app has potential memory leaks.

What about browser support?

You can use these tools in almost all modern browsers: https://caniuse.com/?search=Weak

WeakRef
WeakMap
WeakSet
FinalizationRegistry

Today we have discussed primitive and object types followed weak and strong references, and experimented on how to track the moment when the object is about to be GCed.

Top comments (0)