Memory Leak Risk in JavaScript Closures

Memory leak risk in JavaScript closures

I used to work with the ex-program manager for JavaScript within Microsoft, on a non-browser EcmaScript (err.. JScr... JavaScript) project. We had some lengthy discussions about closures. In the end, the point is that they are harder to GC, not impossible. I'd have to read DC's discussion of how MS is 'wrong' that closures cause memory leaks -- for in IE's older implementations, closures were certainly problematic, because they were very hard to garbage collect with the MS implementation. I find it strange that a Yahoo guy would try to tell the MS architects that a known issue with their code was somewhere else. As much as I appreciate his work, I don't see what basis he had there.

Remember, the article you reference above refers to IE6, as IE7 was still under heavy development at the time of its writing.

As an aside -- thank god, IE6 is dead (don't make my dig up the funeral pictures). Although, don't forget its legacy... I've yet to see anyone make a credible argument that it wasn't the finest browser in the world on day 1 of its release -- the problem was that they won the browser war. And so, in what amounts to one of the larger blunders of their history -- they fired the team afterwards and it sat stagnant for nearly 5 years. The IE team was down to 4 or 5 guys doing bug fixes for years, creating a huge brain drain and falling behind the curve dramatically. By the time they rehired the team and realized where they were, they were years behind because of the added cruft of dealing with a monolothic codebase that nobody really understood anymore. That's my perspective as an internal in the company, but not directly tied to that team.

Remember too, IE never optimized for closures because there was no ProtoypeJS (heck, there was no Rails), and jQuery was barely a glimmer in Resig's head.

At the time of the writing, they were also still targeting machines with 256 megs of RAM, which also hadn't been end-of-life'd.

I thought it only fair to hand you this history lesson, after making me read your entire book of a question.

In the end, my point is that you're referencing material which is hugely dated. Yes, avoid closures in IE6, as they cause memory leaks -- but what didn't in IE6?

In the end, it is a problem that MS has addressed and continues to address. You're going to make some level of closures, that was the case, even back then.

I know that they did heavy work in this area around IE8 (as my unmentionable project used the non-at-the-time standard JavaScript engine), and that work has continued into IE9/10. StatCounter (http://gs.statcounter.com/) suggests that IE7 is down to a 1.5% market share, down from 6% a year ago, and in developing 'new' sites, IE7 becomes less and less relevant. You can also develop for NetScape 2.0, which introduced JavaScript support, but that would be only slightly less silly.

Really... Don't try to over-optimize for the sake of an engine which doesn't exist anymore.

Javascript closures and memory leak risks

You are correct, your second example would use less memory because of less closure functions. But as soon as you event isn't callable (element removed etc.) they would disappear again so it is not a "leak" as the memory isn't lost forever.

Also many plugins use the closure by setting the current state of an element in a variable instead of the element itself.

Memory leaks and closures in JavaScript - when and why?

This question asks about something similar. Basically, the idea is that if you use a closure in a callback, you should "unsubscribe" the callback when you are finished so the GC know that it can't be called again. This makes sense to me; if you have a closure just waiting around to be called, the GC will have a hard time knowing that you're finished with it. By manually removing the closure from the callback mechanism, it becomes unreferenced and available for collection.

Also, Mozilla has published a great article on finding memory leaks in Node.js code. I would assume that if you try out some of their strategies, you could find parts of your code that express leaky behavior. Best practices are nice and all, but I think it's more helpful to understand your program's needs and come up with some personalized best practices based on what you can empirically observe.

Here's a quick excerpt from the Mozilla article:

  • Jimb Esser’s node-mtrace, which uses the GCC mtrace utility to profile heap usage.
  • Dave Pacheco’s node-heap-dump takes a snapshot of the V8 heap and serializes the whole thing out in a huge JSON file. It includes tools to traverse and investigate the resulting snapshot in JavaScript.
  • Danny Coates’s v8-profiler and node-inspector provide Node bindings for the V8 profiler and a Node debugging interface using the WebKit Web Inspector.
  • Felix Gnass’s fork of the same that un-disables the retainers graph
  • Felix Geisendörfer’s Node Memory Leak Tutorial is a short and sweet explanation of how to use the v8-profiler and node-debugger, and is presently the state-of-the-art for most Node.js memory leak debugging.
  • Joyent’s SmartOS platform, which furnishes an arsenal of tools at your disposal for debugging Node.js memory leaks

The answers to this question basically say that you can help the GC out by assigning null to closure variables.

var closureVar = {};
doWork(function callback() {
var data = closureVar.usefulData;
// Do a bunch of work
closureVar = null;
});

Any variables declared inside a function will go away when the function returns, except those that are used in other closures. In this example, closureVar has to be in memory until callback() is called, but who knows when that will happen? Once the callback has been called, you can give a hint to the GC by setting your closure variable to null.

DISCLAIMER: As you can see from the comments below, there are some SO users who say that this information is out of date and inconsequential for Node.js. I don't have a definitive answer on that yet; I'm just posting what I've found on the web.

JavaScript Memory leak from closure lexical environment

The primary answer is that in your second code block, no direct reference to either of the closures (closure1 or someMethod) survives the return of outer (nothing outside outer refers to them), and so there's nothing left that refers to the context where they were created, and that context can be cleaned up. In your second code block, though, a direct reference to someMethod survives the return, as part of the object that you're assigning to aThing, and so the context as a whole cannot be GC'd.

Let's follow what happens with your first block:

After the first execution of outer, we have (ignoring a bunch of details):


+−−−−−−−−−−−−−+
aThing−−−−−>| (object #1) |
+−−−−−−−−−−−−−+
| str: ... | +−−−−−−−−−−−−−−−−−−−−+
| someMethod |−−−−>| (context #1) |
+−−−−−−−−−−−−−+ +−−−−−−−−−−−−−−−−−−−−+
| something: null |
| closure1: function |
+−−−−−−−−−−−−−−−−−−−−+

after the second execution:


+−−−−−−−−−−−−−+
aThing−−−−−>| (object #2) |
+−−−−−−−−−−−−−+
| str: ... | +−−−−−−−−−−−−−−−−−−−−+
| someMethod |−−−−>| (context #2) |
+−−−−−−−−−−−−−+ +−−−−−−−−−−−−−−−−−−−−+ +−−−−−−−−−−−−−+
| something |−−−−>| (object #1) |
| closure1: function | +−−−−−−−−−−−−−+
+−−−−−−−−−−−−−−−−−−−−+ | str: ... | +−−−−−−−−−−−−−−−−−−−−+
| someMethod |−−−−>| (context #1) |
+−−−−−−−−−−−−−+ +−−−−−−−−−−−−−−−−−−−−+
| something: null |
| closure1: function |
+−−−−−−−−−−−−−−−−−−−−+

after the third execution:


+−−−−−−−−−−−−−+
aThing−−−−−>| (object #3) |
+−−−−−−−−−−−−−+
| str: ... | +−−−−−−−−−−−−−−−−−−−−+
| someMethod |−−−−>| (context #3) |
+−−−−−−−−−−−−−+ +−−−−−−−−−−−−−−−−−−−−+ +−−−−−−−−−−−−−+
| something |−−−−>| (object #2) |
| closure1: function | +−−−−−−−−−−−−−+
+−−−−−−−−−−−−−−−−−−−−+ | str: ... | +−−−−−−−−−−−−−−−−−−−−+
| someMethod |−−−−>| (context #2) |
+−−−−−−−−−−−−−+ +−−−−−−−−−−−−−−−−−−−−+ +−−−−−−−−−−−−−+
| something |−−−−>| (object #1) |
| closure1: function | +−−−−−−−−−−−−−+
+−−−−−−−−−−−−−−−−−−−−+ | str: ... | +−−−−−−−−−−−−−−−−−−−−+
| someMethod |−−−−>| (context #1) |
+−−−−−−−−−−−−−+ +−−−−−−−−−−−−−−−−−−−−+
| something: null |
| closure1: function |
+−−−−−−−−−−−−−−−−−−−−+

You can see where this is going.

Since the second block never retains a reference to closure1 or someMethod, neither of them keeps the context in memory.

When originally answering your question in 2015 I was slightly surprised that V8 (Chrome's JavaScript engine) didn't optimize this leak away, since only someMethod is retained, and someMethod doesn't actually use something or closure1 (or eval or new Function or debugger). Although in theory it has references to them via the context, static analysis would show that they can't actually be used and so could be dropped. But closure optimization is really easy to disturb, I guess something in there is disturbing it, or that the V8 team found that doing that level of analysis wasn't worth the runtime cost. I do recall seeing a tweet from one of the V8 team saying that it used to do more closure optimization than it does now (this edit is in Sep 2021) because the trade-off wasn't worth it.

Releasing closures

It was already mentioned here that :on() invocation saves reference to callback closure inside Lua registry.

I guess this closure will be cleared from Lua registry inside __gc metamethod of sock object,

but sock object will not be collected if the closure references sock object.

To solve this problem you should avoid hard-coding a reference to sock upvalue in the body of sendchunk() function.

For example, exploit the fact that the first argument passed to callback function is always the socket object.

function sendfile(sock, name)
local fd = file.open(name, "r")

local function sendchunk(sck)
local data = fd:read()
if data then
sck:send(data)
else
fd:close()
sck:close()
end
end

sock:on("sent", sendchunk)
sendchunk(sock)
end

Why this is a memory leak

The following article is old, and this is not an issue anymore

The issue is described in this article:

window.onload = function() {
var obj = document.getElementById("element");

// this creates a closure over "element"
obj.onclick = function(evt) {
... logic ...
};
};

Here is a diagram describing the closure which creates a circular reference between the DOM world and the JS world.

Sample Image

The above pattern will leak due to closure. Here the closure's global variable obj is referring to the DOM element. In the mean time, the DOM element holds a reference to the entire closure. This generates a circular reference between the DOM and the JS worlds. That is the cause of leakage.

From the MDN web docs: A closure is the combination of a function and the lexical environment within which that function was declared.
In that case the lexical environment is the window.onload function, which includes the obj variable.

examples of closures in node.js that would cause memory leaks

Not a "leak" exactly, but this can be a common pitfall.

var fn = (function() {
var a = "super long string ...";
var b = "useless value";
var c = "Hello, World!";

return function() {
return c;
};
})();

This returns a function that references a scope, and every single local var in that scope will be kept, even though only one of those values are needed. This results in more memory usage than you need, especially if your function uses a small variable, but there are large values in that scope that you dont need to keep referencing.


How to fix it?

Simple option is to null out the variables you dont care about at the end of your function. The variables are still in scope, but their data would be released.

var fn = (function() {
var a = "super long string ...";
var b = "useless value";
var c = "Hello, World!";

// do stuff with a and b

a = b = null;

return function() {
return c;
};
})();

Or you could break anything that uses temp vars into it's own function so their scopes can be released. This is a better solution for a larger project.

var doSetup = function() {
var a = "super long string ...";
var b = "useless value";
// do stuff with a and b
};

var fn = (function() {
doSetup();

var c = "Hello, World!";

return function() {
return c;
};
})();


Related Topics



Leave a reply



Submit