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
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
π 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;
π 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
}
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:
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();
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 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:
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.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
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:
- Lose/remove all strong references to the object
- Allocate a lot of memory and remove strong refs to these objects too
- Make macrotasks pauses to increase chances for the GC
- 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);
});
}
Now let's prepare the test code:
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");
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:
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:
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");
π 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:
const weakRef = new WeakRef(strongRef); // Register our object // remove all StrongRefs from the object // Wait!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 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;
});
registry.register(strongRef, strongRef.field);
strongRef = null;
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
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)