Total Recall
Most of those who don't work with memory management
don't know what it is
Allocate
Use
Release
Regardless of the programming language, memory life cycle is pretty much always the same
Value initialization (when you define literals) *
var n = 123; // allocates memory for a number
var s = 'azerty'; // allocates memory for a string
var o = {
a: 1,
b: null
}; // allocates memory for an object and contained values
// (like object) allocates memory for the array and
// contained values
var a = [1, null, 'abra'];
function f(a) {
return a + 2;
} // allocates a function (which is a callable object)
// function expressions also allocate an object
someElement.addEventListener('click', function() {
someElement.style.backgroundColor = 'blue';
}, false);
Allocation via function calls (hidden memory allocation) *
var d = new Date(); // allocates a Date object
var e = document.createElement('div'); // allocates a DOM element
var s = 'azerty';
var s2 = s.substr(0, 3); // s2 is a new string
// Since strings are immutable value,
// JavaScript may decide to not allocate memory,
// but just store the [0, 3] range.
var a = ['ouais ouais', 'nan nan'];
var a2 = ['generation', 'nan nan'];
var a3 = a.concat(a2);
// new array with 4 elements being
// the concatenation of a and a2 elements
Memory can be represented by a directed Object Graph
var hero = { name: 'John' }
var robot = { answer: 42, root: true }
var serial = 1100
Root
Object value
Scalar value
'John'
{ name: 'John' }
{ answer: 42, root: true}
true
42
1100
window
A value can have one or more retaining paths
Using values basically means reading and writing in allocated memory. This can be done by reading or writing the value of a variable or an object property or even passing an argument to a function *
Memory size after allocation or modification
depends on how Javascript Engine works.
For example there are slow and fast objects in V8
when the memory is not needed anymore
There's no way to manual memory release because
that releases memory we don't need
It depends on the garbage collector algorithm
But what about Javascript Engines and Browsers?
* Generational Garbage Collection in Firefox
** Webkit. Introducing Riptide. Jan 20, 2017
*** ChakraCore. Architecture Overview. Garbage Collector. May 3, 2017
**** V8. Design Elements. Efficient Garbage Collection. July 17, 2017
Javascript Engine | Browsers | Garbage Collector |
---|---|---|
SpiderMonkey | Mozilla Firefox | Generational garbage collection (GGC) has now been enabled in the SpiderMonkey JavaScript engine in Firefox 32 * |
JavaScriptCore | Safari | Wavefront Concurrent Garbage Collector. it's Guy Steele’s classic retreating wavefront write barrier with a mature sticky-mark-sweep collector ** |
ChakraCore | Microsoft Edge | Mark-and-sweep garbage collector that supports concurrent and partial collections *** |
V8 | Chromium, Chrome, Opera, Vivaldi | Stop-the-world, generational, accurate, garbage collector **** |
Algorithm actual for IE 6 and IE 7 (but IE is not actual now)
Let's investigate the code below
// window is a root object in the browser javascript
window.a1 = { a2: {} }
// create circular reference
window.a1.a2.parentReference = window.a1
window.b1 = { b2: 2017 }
window.c1 = { c2: {} }
window.a1 = null // remove a1 object reference
window.c1.c2 = null // remove c2 object reference
Root
Object value
Scalar value
A
B
C
Represent the code as object graph with reference counters
window
A1
A2
B2
B1
C1
C2
1
1
1
1
2
1
Root
Object value
Scalar value
A
B
C
Remove references
window
A1
A2
B2
B1
C1
C2
1
1
1
1
1
0
Root
Object value
Scalar value
A
B
C
Delete objects without references
window
A1
A2
B2
B1
C1
C2
0
1
1
1
1
1
Root
Object value
Scalar value
A
B
C
Result of reference-counting garbage collector working
window
A1
A2
B2
B1
C1
As of 2012, all modern browsers ship a mark-and-sweep garbage-collector **
All improvements made in the field of JavaScript garbage collection over the last few years are implementation improvements of this algorithm
Let's investigate the code below
// window is a root object in the browser javascript
window.a1 = { a2: {} }
// create circular reference
window.a1.a2.parentReference = window.a1
window.b1 = { b2: 2017 }
window.c1 = { c2: {} }
window.a1 = null // remove a1 object reference
window.c1.c2 = null // remove c2 object reference
Root
Object value
Scalar value
A
B
C
Represent the code as object graph
window
A1
A2
B2
B1
C1
C2
Root
Object value
Scalar value
A
B
C
Remove references
window
A1
A2
B2
B1
C1
C2
Root
Object value
Scalar value
A
B
C
Mark garbage collector phase
window
A1
A2
B2
B1
C1
C2
* Objects are allocated into JS Heap. For example there're Heap and HeapObject in V8 Engine
Root
Object value
Scalar value
A
B
C
Sweep garbage collector phase
window
B2
B1
C1
Root
Object value
Scalar value
A
B
C
Result of mark-and-sweep garbage collector working
window
B2
B1
C1
dependent on the garbage collector algorithm
It means one cannot be certain when a collection will be performed.
Most GC implementations share the common pattern of doing collection passes during allocation *
Occurs when program acquires memory but fails to release it back to the operating system.
It looks like a woman's wardrobe
The woman is a program
The dress is allocated memory
The size wardrobe is limited allowed memory
More and more dresses, only a few used
Page's memory consumption will continue growing
Memory is allocated only once (one-time leak)
based on memory growing characteristic
is not only memory you don't release,
but also memory you don't need anymore
V8: JS Heap, DOM Nodes, Event Listeners
Is there a memory leak? No thanks!
setInterval(function(){
console.log("Remember! There's no memory leak")
}, 10);
Code is not as scary as performance profile
Is there a memory leak? Perhaps!
Is there a memory leak? Definitely!
Be sure that GC minor and GC major were called both
* First used by Loreena Lee in the Gmail team
User can push the button only
There's the staircase pattern! Memory leak!?
There are 100 new HTML Elements and Text Nodes
patterns are based on source of problem
var foo = document.getElementById("foo")
var bar = document.getElementById("bar")
foo.element = bar
bar.element = foo
foo.parentNode.removeChild(foo)
bar.parentNode.removeChild(bar)
foo = null
bar = null
var foo = document.getElementById("foo")
var bar = document.getElementById("bar")
foo.element = bar
bar.element = foo
foo.parentNode.removeChild(foo)
bar.parentNode.removeChild(bar)
foo.element = null
bar.element = null
foo = null
bar = null
How to Fix
Example of Code
(function(window) {
cache = []
window.CacheService = function() {
return {
cache: function(value) {
cache.push(new Array(1000).join('*'))
cache.push(value)
}
}
}
})(window)
var service = new window.CacheService()
for (let i=0; i < 99999; i++) {
service.cache(i)
}
service = null
Example of Code
Performance profile
* GC was manually called between 7 and 9 seconds
Cache Service Example
(function(window) {
window.CacheService = function() {
var cache = []
return {
cache: function(value) {
cache.push(new Array(1000).join('*'))
cache.push(value)
}
}
}
})(window)
var service = new window.CacheService()
for (let i=0; i < 99999; i++) {
service.cache(i)
}
service = null
Performance profile
* GC was manually called between 6 and 7 seconds
function checkStatus() {
fetch('/endpoint').then(function(response) {
var container = document.getElementById("container");
container.innerHTML = response.status;
container.addEventListener("mouseenter", function mouseenter() {
container.innerHTML = response.statusText;
});
container.addEventListener("mouseout", function mouseout() {
container.innerHTML = response.status;
});
})
}
setInterval(checkStatus, 100)
<div id="container" />
Fetch available status of endpoint
Performance profile
Callbacks for event listener are created each 100ms
var container = document.getElementById("container");
setInterval(function() {
container.addEventListener("mouseout", function mouseout() {
// some code here
});
}, 100)
Performance profile
var container = document.getElementById("container");
var status = {
code: null,
text: null
}
container.addEventListener("mouseenter", function() {
container.innerHTML = status.code;
});
container.addEventListener("mouseout", function() {
container.innerHTML = status.text;
});
function processResponse(response) {
status.code = response.status
status.text = response.statusText
}
function checkStatus() {
fetch('/endpoint').then(processResponse)
}
setInterval(checkStatus, 100)
How to fix
Performance profile
function(scope, element, attrs) {
element.on('click', function() {
scope.selected = true
})
}
Poorly Managed Event Handlers in directives *
function(scope, element, attrs) {
element.on('click', function() {
scope.selected = true
})
scope.$on('$destroy', function() {
element.off() // deregister all event handlers
})
}
How to Fix
$scope.$on('someEvent', function() {
$scope.refresh()
}
Poorly Managed Watchers *
var cleanup = $scope.$on('someEvent', function() {
$scope.refresh()
})
$scope.$on('$destroy', function() {
cleanup()
})
How to Fix
var strangeObject = {
callAgain: function () {
var ref = this
var val = setTimeout(function () {
ref.callAgain()
}, 50)
}
}
strangeObject.callAgain()
strangeObject = null
Performance profile of code
It there a memory leak?
var strangeObject = {
storage: [],
callAgain: function () {
var ref = this
ref.storage.push(new Array(1000000).join('*'))
var val = setTimeout(function () {
ref.callAgain()
}, 50)
}
}
strangeObject.callAgain()
strangeObject = null
Add special property to store values in strangeObject
Performance profile of code
How to fix
var strangeObject = {
storage: [],
startCallAgain: function() {
this.interval = setInterval(this.tickCallAgain.bind(this), 50)
},
tickCallAgain: function() {
this.storage.push(new Array(1000000).join('*'))
},
stopCallAgain: function() {
if (this.interval) {
clearInterval(this.interval)
}
}
}
strangeObject.startCallAgain()
setTimeout(function() {
strangeObject.stopCallAgain()
strangeObject = null
}, 5000)
Performance profile
class IntervalComponent extends React.Component {
// ..
handleInterval() {
console.log('boom', this.props.id)
}
componentDidMount() {
setInterval(this.handleInterval, 1000);
}
// ..
}
class IntervalComponent extends React.Component {
// ..
handleInterval() {
console.log('boom', this.props.id)
}
componentDidMount() {
this.interval = setInterval(this.handleInterval, 1000)
}
componentWillUnmount() {
clearInterval(this.interval)
}
// ..
}
How to Fix
var elements = {
container: document.querySelector('#container'),
form: document.querySelector('form'),
submit: document.querySelector('[type="submit"]')
};
elements.form.addEventListener('submit', function(e) {
e.preventDefault();
elements.container.innerHTML = 'Ops..';
})
Static memory leak
3 snapshot technique useless here
<div id="container">
<form>
<input type="submit" value="Push me" />
</form>
</div>
var elements = {
container: document.querySelector('#container'),
form: document.querySelector('form'),
submit: document.querySelector('[type="submit"]')
}
function processSubmit(e) {
e.preventDefault()
elements.form.removeEventListener('submit', processSubmit)
elements.container.innerHTML = 'Ops..'
elements = {
container: document.querySelector('#container')
}
}
elements.form.addEventListener('submit', processSubmit)
How to fix
function f() {
var some = [];
while(some.length < 1e6) {
some.push(some.length);
}
function unused() { some; } //causes massive memory leak
return function() {};
}
var a = [];
var interval = setInterval(function() {
var len = a.push(f());
document.getElementById('count').innerHTML = len.toString();
if (len >= 500) {
clearInterval(interval);
}
}, 10);
Performance profile of code
Memory can't be released because it can't be allocated before
function f() {
return function() {};
}
var a = [];
var interval = setInterval(function() {
var len = a.push(f());
document.getElementById('count').innerHTML = len.toString();
if (len >= 500) {
clearInterval(interval);
}
}, 10);
How to Fix? Investigate and refactor the code
var theThing = null
var replaceThing = function () {
var originalThing = theThing
var unused = function () {
if (originalThing)
console.log("hi")
}
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log(someMessage)
}
}
}
setInterval(replaceThing, 1000)
JavaScript memory leak in Meteor’s live HTML template rendering system
Performance profile
var theThing = null
var replaceThing = function () {
var originalThing = theThing
var unused = function () {
if (originalThing)
console.log("hi")
}
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log(someMessage)
}
}
originalThing = null;
}
setInterval(replaceThing, 1000)
How to Fix? Just null the originalThing object
Performance profile
setInterval(function(){
var xhr = new XMLHttpRequest();
xhr.open('GET', 'json.txt', true);
xhr.onreadystatechange = function() {
if(this.readyState == 4 && this.status == 200) {
console.log(this.responseText);
}
};
xhr.send('');
}, 500);
var patt1 = new RegExp(
"((.*)(((((((((((((((((((((((((((((((.*(.*(.*(.*" +
"(.*(.*(.*(.*(.*(.*(.*(.*(.*(.*(.*(.*(.*(.*(.*(.*" +
"(.*(.*(.*(.*(.*(.*(.*(.*(.*(.*(.*){10}(.*){10}" +
"(.*){10}(.*){10}(.*){10}(.*){10}(.*){10}(.*){10}" +
"(.*){10}(.*){10}(.*){10}(.*){10}(.*){10}(.*){10}" +
"(.*){10}(.*){10}(.*){10}(.*){10}(.*){10}(.*){10}" +
"(.*){10}(.*){10}(.*){10}(.*){10}(.*){10}(.*){10}" +
"(.*){10}(.*){10}(.*){10}(.*){10}.*)+).*)+).*)+).*" +
")+).*)+).*)+).*)+).*)+).*)+).*)+).*)+).*)+).*)+).*" +
")+).*)+).*)+).*)+).*)+).*)+).*)+).*)+).*)+).*)+).*" +
")+).*)+).*)+).*)+).*)+).*)+).*)+))"
)
document.write(patt1.exec("peace"))
Then
Then
References to objects in the collection and maps are held weakly
WeakMaps* and WeakSets** are not enumerable
const weakMap = new WeakMap()
let fruits = ['apple', 'orange', 'banana']
weakMap.set(fruits, 'value')
console.log(weakMap.has(fruits)) // output: true
fruits = null
console.log(weakMap.has(fruits)) // output: false
It's a possible problem of application growing up