40

I would like to understand what kind of code causes memory leaks in JavaScript and created the script below. However, when I run the script in Safari 6.0.4 on OS X the memory consumption shown in the Activity Monitor does not really increase.

Is something wrong with my script or is this no longer an issue with modern browsers?

<html>
<body>
</body>
<script>
var i, el;

function attachAlert(element) {
    element.onclick = function() { alert(element.innerHTML); };
}

for (i = 0; i < 1000000; i++) {
    el = document.createElement('div');
    el.innerHTML = i;
    attachAlert(el);
}
</script>
</html>

The script is based on the Closure section of Google's JavaScript style guide: http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml?showone=Closures#Closures

EDIT: The bug that caused the above code to leak has apparently been fixed: http://jibbering.com/faq/notes/closures/#clMem

But my question remains: Would someone be able to provide a realistic example of JavaScript code that leaks memory in modern browsers?

There are many articles on the Internet that suggest memory leaks can be an issue for complex single page applications but I have a hard time finding an examples that I can run in my browser.

1

8 Answers 8

10

You're not keeping the element you've created around and referenced anywhere - that's why you're not seeing the memory usage increase. Try attaching the element to the DOM, or store it in an object, or set the onclick to be a different element that sticks around. Then you'll see the memory usage skyrocket. The garbage collector will come through and clean up anything that can no longer be referenced.

Basically a walkthrough of your code:

  • create element (el)
  • create a new function that references that element
  • set the function to be the onclick of that element
  • overwrite the element with a new element

Everything is centric around the element existing. Once there isn't a way to access the element, the onclick can't be accessed anymore. So, since the onclick can't be accessed, the function that was created is destroyed.. and the function had the only reference to the element.. so the element is cleaned up as well.

Someone might have a more technical example, but that's the basis of my understanding of the javascript garbage collector.

Edit: Here's one of many possibilities for a leaking version of your script:

<html>
<body>
</body>
<script>
var i, el;

var createdElements = {};
var events = [];

function attachAlert(element) {
    element.onclick = function() { alert(element.innerHTML); };
}

function reallyBadAttachAlert(element) {
    return function() { alert(element.innerHTML); };
}

for (i = 0; i < 1000000; i++) {
    el = document.createElement('div');
    el.innerHTML = i;

    /** posibility one: you're storing the element somewhere **/
    attachAlert(el);
    createdElements['div' + i] = el; 

    /** posibility two: you're storing the callbacks somewhere **/
    event = reallyBadAttachAlert(el);
    events.push(event);
    el.onclick = event;

}
</script>
</html>

So, for #1, you're simply storing a reference to that element somewhere. Doesn't matter that you'll never use it - because that reference is made in the object, the element and its callbacks will never go away (or at least until you delete the element from the object). For possibility #2, you could be storing the events somewhere. Because the event can be accessed (i.e. by doing events[10]();) even though the element is nowhere to be found, it's still referenced by the event.. so the element will stay in memory as well as the event, until it's removed from the array.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for your reply! Would you be able to post a modified version of my script for illustration?
Done. Hopefully that helps.
Thanks, but I do not think this is a relevant example. If I store references to all elements that are created I do not expect them to be garbage collected. The point of my code what that the closure creates a circular reference. Circular references are normally no problem for the garbage collector but - as I now learned - there was a bug where the garbage collection would not work in certain browsers when one of the objects involved in the circular reference was a DOM element.
this example no longer creates memory leak
3
+25

update: Here is a very simple example based on the caching scenario in the Google I/O presentation:

/*
This is an example of a memory leak. A new property is added to the cache
object 10 times/second. The value of performance.memory.usedJSHeapSize
steadily increases.

Since the value of cache[key] is easy to recalculate, we might want to free
that memory if it becomes low. However, there is no way to do that...

Another method to manually clear the cache could be added, but manually
adding memory checks adds a lot of extra code and overhead. It would be
nice if we could clear the cache automatically only when memory became low.

Thus the solution presented at Google I/O!
*/

(function(w){
    var cache = {}
    function getCachedThing(key) {
        if(!(key in cache)) {
            cache[key] = key;
        }
        return cache[key];
    }

    var i = 0;
    setInterval(function() {
        getCachedThing(i++);
    }, 100);
    w.getCachedThing = getCachedThing
})(window);

Because usedJSHeapSize does not update when the page is opened from the local file system, you might not see the increasing memory usage. In that case, I have hosted this code for you here: https://memory-leak.surge.sh/example-for-waterfr


This Google I/O'19 presentation gives examples of real-world memory leaks as well as strategies for avoiding them:

  • Method getImageCached() returns a reference to an object, also caching a local reference. Even if this reference goes out of the method consumer's scope, the referenced memory cannot be garbage collected because there is a still a strong reference inside the implementation of getImageCached(). Ideally, the cached reference would be eligible for garbage collection if memory got too low. (Not exactly a memory leak, but a situation where there is memory that could be freed at the cost of running the expensive operations again).
  • Leak #1: the reference to the cached image. Solved by using weak references inside getImageCached().
  • Leak #2: the string keys inside the cache (Map object). Solved by using the new FinalizationGroup API.

Please see the linked video for JS code with line-by-line explanations.

More generally, "real" JS memory leaks are caused by unwanted references (to objects that will never be used again). They are usually bugs in the JS code. This article explains four common ways memory leaks are introduced in JS:

  1. Accidental global variables
  2. Forgotten timers/callbacks
  3. Out of DOM references
  4. Closures

An interesting kind of JavaScript memory leak documents how closures caused a memory leak in the popular MeteorJS framework.

7 Comments

Please provide a working example! We can search google ourselves!
@WaterfrVilla: Live example at memory-leak.surge.sh/example-for-walterfr (Also updated answer.)
Please in a loop or something with the performance.memory.usedJSHeapSize getting updated and displayed
@WaterfrVilla: if you open the dev console, you can inspect usedJSHeapSize. Also, I already made two other examples for you: memory-leak.surge.sh and memory-leak.surge.sh/simple which show usedJSHeapSize getting updated and displayed
But, this is not a memory leak!
|
2

2020 Update:

Most CPU-side memory overflow is no longer working on modern v8 engine based browser. However, we can overflow the GPU-side memory by running this script

// Initialize canvas and its context
window.reallyFatCanvas = document.createElement('canvas');
let context = window.reallyFatCanvas.getContext('2d');

// References new context inside context, in loop.
function leakingLoop() {
    context.canvas.width = document.body.clientWidth;
    context.canvas.height = document.body.clientHeight;
    const newContext = document.createElement('canvas').getContext('2d');
    context.context = newContext;
    context.drawImage(newContext.canvas, 0, 0);
    
    // The new context will reference another context on the next loop
    context = newContext;
}

// Use interval instead of while(true) {...}
setInterval(leakingLoop,1);

EDIT: I rename every variables (and constants) so it makes a lot of sense. Here is the explanation.

Based on my observation, canvas context seems sync with Video Memory. So if we put reference of a canvas object which also reference another canvas object and so on, the Video RAM fills a lot more than DRAM, tested on microsoft edge and chrome.

This is my third attempt of screenshot: VRAM Leak with JavaScript

I have no idea why my laptop always freeze seconds after taking screenshot while running this script. Please be careful if you want to try that script.

2 Comments

Maybe consider adding an explanation as to what causes the memory leak given this technical topic.
@dantechguy I tried my best observating why this script causes VRAM leak, please review my edited answer. Thanks!
0

I tried to do something like that and got exception out of memory.

const test = (array) => {
  array.push((new Array(1000000)).fill('test'));
};

const testArray = [];

for(let i = 0; i <= 1000; i++) {
  test(testArray);
}

1 Comment

But, this is not a memory leak!
-1

A small example of code causing a 1MB memory leak:

Object.defineProperty(globalThis, Symbol(), {value: new Uint8Array(1<<20).slice(), writable: false, configurable: false})

After you run that code, the only way to free the leaked memory is to close the tab you ran it on.

1 Comment

Still reachable and freeable with Object.getOwnPropertySymbols.
-1

const bytes = new Uint8Array(100).slice(0, 1);

I have to admit this a not a true memory leak, but it's logically very implicit leaking if you no longer access bytes.buffer.

Comments

-2

If all you want is to create a memory leak, then the easiest way IMO is to instantiate a TypedArray since they hog up a fixed size of memory and outlive any references. For example, creating a Float64Array with 2^27 elements consumes 1GiB (1 Gibibyte) of memory since it needs 8 bytes per element.

Start the console and just write this:

new Float64Array(Math.pow(2, 27))

1 Comment

But, this is not a memory leak!
-3

The Easiest Way Is:

while(true){}

2 Comments

This will create high CPU.
But, this is not a memory leak!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.