Closure memory leaks

# David Bruant (14 years ago)

After reading an article [1], I've been thinking about scopes in ECMAScript. The idea in the article is that a closure keeps a reference to everything in its scope. If in turn, one element in the scope keeps a reference to the function (like as an event listener for an element), then, it creates a circular reference.

Of course, as said in a couple of comments, circular references aren't a problem since modern GC (IE8+ and other relevant browsers) aren't reference-counting GC. But the problem could exist anyway if, for instance a function has in its scope an object it doesn't use. Example:

"use strict"; function f(){ var o = {}; // In g's scope, but unused anyway return function g(){ return 1; }; }

I originally thought that a static analysis of the closure could be enough to determine whether things in the scope will ever be used or not. It turns out it's not the case:

"use strict"; function f(){ var o = {a:1}; return function(e){ // doesn't strictly use o return eval(e); }; } console.log( f()('o') ); // returns the enclosed o object (at least on FF4)

So, this leads to a couple of questions:

  • Is the previous code snippet correct?
  • In which cases can static analysis be enough?
  • Are there other forms of analysis that automatically prevent scope memory leaks?
  • Are there good practices people could use to avoid such leaks?
  • Are "let" or "const" of any help here?
  • If they aren't, would it make sense to have an initializer keyword to create variables which could NOT be captured by closures? It would avoid memory leak, but also would avoid to leak implementation details.

Thanks,

David

[1] atkinson.posterous.com/javascript

# Mike Samuel (14 years ago)

2011/5/22 David Bruant <david.bruant at labri.fr>:

Hi,

After reading an article [1], I've been thinking about scopes in ECMAScript. The idea in the article is that a closure keeps a reference to everything in its scope. If in turn, one element in the scope keeps a reference to the function (like as an event listener for an element), then, it creates a circular reference.

Of course, as said in a couple of comments, circular references aren't a problem since modern GC (IE8+ and other relevant browsers) aren't reference-counting GC. But the problem could exist anyway if, for instance a function has in its scope an object it doesn't use. Example:

"use strict"; function f(){     var o = {}; // In g's scope, but unused anyway     return function g(){                return 1;            }; }

I originally thought that a static analysis of the closure could be enough to determine whether things in the scope will ever be used or not. It turns out it's not the case:

"use strict"; function f(){     var o = {a:1};     return function(e){ // doesn't strictly use o                return eval(e);            }; } console.log( f()('o') ); // returns the enclosed o object (at least on FF4)

So, this leads to a couple of questions:

  • Is the previous code snippet correct?
  • In which cases can static analysis be enough?

When the name "eval" is not mentioned static analysis can conclude what needs to be captured by a closure and what doesn't.

If "eval" is aliased, it binds in the global scope.

Static analysis that can prove that a closure mentioning "eval" has a lifetime that is bounded by the call of the function in which it is declared can also be ignored. So if "eval" can't capture arguments.callee because it's strict, and the function has no name, and does not escape due to non-eval usage, then variables don't need to be pinned.

  • Are there other forms of analysis that automatically prevent scope memory leaks?
  • Are there good practices people could use to avoid such leaks?
  • Are "let" or "const" of any help here?

Let and const can help in this case:

function outer() { function closure(s) { return eval(s); }

for (...) { let largeObject = ...; }

return closure; }

if largeObject were declared var, then closure would have access to it so it would have to be pinned.

# Brendan Eich (14 years ago)

On May 22, 2011, at 3:01 PM, Mike Samuel wrote:

2011/5/22 David Bruant <david.bruant at labri.fr>:

But the problem could exist anyway if, for instance a function has in its scope an object it doesn't use. Example:

"use strict"; function f(){ var o = {}; // In g's scope, but unused anyway return function g(){ return 1; }; }

SpiderMonkey and I believe other competitive implementations do not entrain the outer environment in such a case.

I originally thought that a static analysis of the closure could be enough to determine whether things in the scope will ever be used or not. It turns out it's not the case:

Not ever? Categorical thinking is not helpful when optimizing.


"use strict"; function f(){ var o = {a:1}; return function(e){ // doesn't strictly use o return eval(e); }; } console.log( f()('o') ); // returns the enclosed o object (at least on FF4)

So, this leads to a couple of questions:

  • Is the previous code snippet correct?
  • In which cases can static analysis be enough?

When the name "eval" is not mentioned static analysis can conclude what needs to be captured by a closure and what doesn't.

Right, and eval is relatively rare.

One can go further and do optimistic type inference, redoing it if eval upsets the party. This requires re-JITting, "on stack replacement", and the like. All doable (and in progress in at least one VM).

  • Are there other forms of analysis that automatically prevent scope memory leaks?
  • Are there good practices people could use to avoid such leaks?
  • Are "let" or "const" of any help here?

Let and const can help in this case:

function outer() { function closure(s) { return eval(s); }

for (...) { let largeObject = ...; }

return closure; }

if largeObject were declared var, then closure would have access to it so it would have to be pinned.

Simpler:

function maker(a, b, c) { return function (x) { return (a * x + b) * x + c; }; }

At least SpiderMonkey optimizes the returned closure into a "flat closure" (after Chez Scheme's "display closures") by copying a, b, and c into the closure evaluated from the return expression. No environment entraining at all.

Chez Scheme went further and did assignment analysis and heap boxing of individual entrained mutable vars. There's also "lambda lifting".

Optimization opportunities abound, it's really not fair to say that JS closures necessarily (eval is not always necessary) mean memory leaks.