So, let's talk about memory
Think of memory as a graph
Three primitive types:
Numbers (e.g, 3.14159...)
Booleans (true or false)
Strings (e.g, "Werner Heisenberg")
They cannot reference other values.
They are always leafs or terminating nodes.
Everything else is an "Object"
Objects are associative arrays (maps or dictionaries)
So, the object is composed of a collection of (key, value) pairs
And what about Arrays?
An Array is an Object
with numeric keys.
The memory graph
starts with a root
It may be the window object of the browser, or the Global object of a Node.js module.
You don't control how this root object is GC
What does get GC?
Whatever is not reachable from the root.
Retaining path
We call a retaining path any path from GC roots to a particular object
Dominators
Node 1 dominates node 2
Node 2 dominates nodes 3, 4 and 6
Node 3 dominates node 5
Node 5 dominates node 8
Node 6 dominates node 7
Some facts about
the V8 Garbage Collector
Generational
Collector
Age of a value
Young Generation
Old Generation
The age of a value: number of bytes allocated since it was allocated.
- Splited in two spaces: named "to" and "from"
- "to space": very fast allocation
- filling the "to space" triggers a collection:
- "to" and "from" swap
- maybe promotion to old generation
- ~10ms (remember 60fps -> ~16ms)
- Old generation collection is slow.
Remember: triggering a collection pauses your application.
Some de-reference
common errors
Be careful wit the delete keyword.
"o" becomes an SLOW object.
var o = {x:"y"};
delete o.x;
o.x;
//undefined
var o = {x:"y"};
o = null;
o.x; //TypeError
It is better to set "null".
Only when the last reference to an object is removed, is that object eligible for collection.
A word on "slow" objects
-
V8 optimizing compiler makes assumptions on your code to make optimizations.
-
It transparently creates hidden classes that represent your objects.
-
Using this hidden classes, V8 works much faster. If you "delete" properties, these assumptions are no longer valid, and the code is de-optimized, slowing your code.
Fast Object
Slow Object
function SlowPurchase(units, price) {
this.units = units;
this.price = price;
this.total = 0;
this.x = 1;
}
var slow = new SlowPurchase(3, 25);
//x property is useless
//so I delete it
delete slow.x;
"slow" should be using a smaller memory footprint than "fast" (1 less property), shouldn't it?
function FastPurchase(units, price) {
this.units = units;
this.price = price;
this.total = 0;
this.x = 1;
}
var fast = new FastPurchase(3, 25);
"fast" objects are faster
REALITY: "SLOW" is using 15 times more memory
Timers
Timers are a very common source of memory leaks.
Look at the following code:
If we run:
With this we have a memory leak:
var
buggyObject = {
callAgain: function () {
var ref = this;
var val = setTimeout(function () {
console.log('Called again: '
+ new Date().toTimeString());
ref.callAgain();
}, 1000);
}
};
buggyObject.callAgain();
buggyObject = null;
Closures
Closures can be another source of memory leaks. Understand what references are retained in the closure.
And remember: eval is evil
var a = function () {
var largeStr =
new Array(1000000).join('x');
return function () {
return largeStr;
};
}();
var a = function () {
var smallStr = 'x',
largeStr =
new Array(1000000).join('x');
return function (n) {
return smallStr;
};
}();
var a = function () {
var smallStr = 'x',
largeStr =
new Array(1000000).join('x');
return function (n) {
eval(''); //maintains reference to largeStr
return smallStr;
};
}();
DOM leaks are bigger than you think
When is the #tree GC?
var select = document.querySelector;
var treeRef = select("#tree");
var leafRef = select("#leaf");
var body = select("body");
body.removeChild(treeRef);
//#tree can't be GC yet due to treeRef
treeRef = null;
//#tree can't be GC yet, due to
//indirect reference from leafRef
leafRef = null;
//NOW can be #tree GC
#leaf maintains a reference to it's parent (parentNode), and recursively up to #tree, so only when leafRef is nullified is the WHOLE tree under #tree candidate to be GC
Rules of thumb
Use appropiate scope
Unbind event listeners
Manage local cache
Better than de-referencing, use local scopes.
Unbind events that are no longer needed, specially if the related DOM objects are going to be removed.
Be careful with storing large chunks of data that you are not going to use.
E
Object Pools
Young generation GC takes about 10ms.
Maybe it is too much time for you:
Instead of allocating and deallocating
objects, reuse them with object pools.
Note: object pools have their own drawbacks
(for example, cleaning used objects)
Three key questions
-
Are you using
too much memory?
- Do you have
memory leaks?
- Is your app GCing
too often?
Browser Info
You can measure how your users are using memory.
You can monitor their activity to detect unexpected use of memory
(only in Chrome)
> performance.memory
MemoryInfo {
jsHeapSizeLimit: 793000000, usedJSHeapSize: 27600000, totalJSHeapSize: 42100000
}
jsHeapSizeLimit
usedJSHeapSize
totalJSHeapSize
the amount of memory that JavaScript heap is limited to
the amount of memory that JavaScript has allocated (including free space)
the amount of memory currently being used
If usedJSHeapSize grows close to jsHeapSizeLimit there is a risk of:
Chrome DevTools
Ctrl+Shift+I
⌥⌘I
Memory Profiling
Taking snapshots
Reading your results
Summary
EYE-CATCHING THINGS
IN THE SUMMARY
Distance:
distance from the GC root.
If almost all the objects of the same type
are at the same distance,
and a few are at a bigger distance,
that's something worth investigating.
Are you leaking the latter ones?
MORE EYE-CATCHING THINGS
IN THE SUMMARY
Retaining memory:
the memory used by the objects
AND
the objects they are referencing.
Use it to know where are you
using most of the memory.
A TIP ABOUT CLOSURES
It helps a lot to name the functions, so you easily distinguish between closures in the snapshot.
function createLargeClosure() {
var largeStr = new Array(1000000).join('x');
var lC = function() { //this IS NOT a named function
return largeStr;
};
return lC;
}
function createLargeClosure() {
var largeStr = new Array(1000000).join('x');
var lC = function lC() { //this IS a named function
return largeStr;
};
return lC;
}
Switching between snapshots views
Summary: groups by constructor name
Comparison: compares two snapshots
Containment: bird's eye view of the object structure
Dominators: useful to find accumulation points
Understanding node colors
Yellow : object has a JavaScript reference on it
Red : detached node. Referenced from one
with yellow background
You can force GC from Chrome DevTools
When taking a Heap Snapshot, it is automatically forced.
In Timeline, it can be very convenient to force a GC.
Memory leak pattern
Some nodes are not being collected: