Como os fechamentos de JavaScript são coletados de lixo

168

Registrei o seguinte bug do Chrome , o que levou a muitos vazamentos de memória sérios e não óbvios no meu código:

(Esses resultados usam o criador de perfil de memória das Ferramentas de Desenvolvimento do Chrome , que executa o GC e, em seguida, tira um instantâneo de tudo o que não é coletado em lixo.)

No código abaixo, a someClassinstância é coletada como lixo (boa):

var someClass = function() {};

function f() {
  var some = new someClass();
  return function() {};
}

window.f_ = f();

Mas não será coletado lixo neste caso (ruim):

var someClass = function() {};

function f() {
  var some = new someClass();
  function unreachable() { some; }
  return function() {};
}

window.f_ = f();

E a captura de tela correspondente:

captura de tela do Chromebug

Parece que um fechamento (neste caso function() {}) mantém todos os objetos "vivos" se o objeto for referenciado por qualquer outro fechamento no mesmo contexto, independentemente de esse fechamento ser ou não alcançável.

Minha pergunta é sobre a coleta de lixo de fechamento em outros navegadores (IE 9+ e Firefox). Estou familiarizado com as ferramentas do webkit, como o criador de perfil de heap JavaScript, mas conheço pouco das ferramentas de outros navegadores, portanto não pude testar isso.

Em qual desses três casos o IE9 + e o Firefox coletam a someClass instância?

Paul Draper
fonte
4
Para os não iniciados, como o Chrome permite testar quais variáveis ​​/ objetos são coletados como lixo e quando isso acontece?
Nnnnnn
1
Talvez o console esteja mantendo uma referência a ele. Ele é GCed quando você limpa o console?
David #
1
@ David No último exemplo, a unreachablefunção nunca é executada, portanto nada é realmente registrado.
James Montagne
1
Estou tendo problemas para acreditar que um bug dessa importância passou, mesmo que pareçamos nos deparar com os fatos. No entanto, estou olhando o código repetidamente e não encontro nenhuma outra explicação racional. Você tentou não executar o código no console corretamente (ou seja, deixar o navegador executá-lo naturalmente a partir de um script carregado)?
plalx
1
@ Alguns, eu li esse artigo antes. Ele tem o subtítulo "Manuseando referências circulares em aplicativos JavaScript", mas a preocupação das referências circulares JS / DOM não se aplica a nenhum navegador moderno. Menciona encerramentos, mas em todos os exemplos, as variáveis ​​em questão ainda estavam em uso possível pelo programa.
Paul Draper

Respostas:

78

Até onde eu sei, isso não é um bug, mas o comportamento esperado.

Na página de gerenciamento de memória da Mozilla : "A partir de 2012, todos os navegadores modernos lançam um coletor de lixo de marcação e varredura". "Limitação: os objetos precisam ser explicitamente inacessíveis " .

Nos seus exemplos em que a falha someainda é alcançável no fechamento. Eu tentei duas maneiras de torná-lo inacessível e ambos funcionam. Ou você define some=nullquando não precisa mais, ou define window.f_ = null;e desaparece.

Atualizar

Eu tentei no Chrome 30, FF25, Opera 12 e IE10 no Windows.

O padrão não diz nada sobre a coleta de lixo, mas fornece algumas pistas do que deve acontecer.

  • Seção 13 Definição de função, etapa 4: "Deixe o fechamento ser o resultado da criação de um novo objeto Function, conforme especificado em 13.2"
  • Seção 13.2 "um ambiente léxico especificado pelo escopo" (escopo = encerramento)
  • Seção 10.2 Ambientes lexicais:

"A referência externa de um ambiente Lexical (interno) é uma referência ao ambiente Lexical que envolve logicamente o ambiente Lexical interno.

Um Ambiente Lexical externo pode, é claro, ter seu próprio Ambiente Lexical externo. Um ambiente lexical pode servir como ambiente externo para vários ambientes lexicais internos. Por exemplo, se uma Declaração de Função contiver duas Declarações de Função aninhadas , os Ambientes Lexicais de cada uma das funções aninhadas terão como Ambiente Lexical externo o Ambiente Lexical da execução atual da função circundante. "

Portanto, uma função terá acesso ao ambiente do pai.

Portanto, somedeve estar disponível no fechamento da função de retorno.

Então, por que nem sempre está disponível?

Parece que o Chrome e o FF são inteligentes o suficiente para eliminar a variável em alguns casos, mas no Opera e no IE a somevariável está disponível no fechamento (NB: para visualizar esse conjunto, um ponto de interrupção return nulle verificar o depurador).

O GC pode ser aprimorado para detectar se someé usado ou não nas funções, mas será complicado.

Um mau exemplo:

var someClass = function() {};

function f() {
  var some = new someClass();
  return function(code) {
    console.log(eval(code));
  };
}

window.f_ = f();
window.f_('some');

No exemplo acima, o GC não tem como saber se a variável é usada ou não (código testado e funciona no Chrome30, FF25, Opera 12 e IE10).

A memória será liberada se a referência ao objeto for quebrada, atribuindo outro valor a window.f_.

Na minha opinião, isso não é um bug.

alguns
fonte
4
Porém, quando o setTimeout()retorno de chamada é executado, o escopo da função do setTimeout()retorno de chamada é concluído e todo o escopo deve ser coletado como lixo, liberando sua referência a some. Não há mais nenhum código que possa ser executado que possa alcançar a instância do someencerramento. Deve ser recolhido lixo. O último exemplo é ainda pior, porque unreachable()nem é chamado e ninguém faz referência a ele. Seu escopo também deve ser analisado. Ambos parecem bugs. Não há requisito de linguagem no JS para "liberar" coisas em um escopo de função.
jfriend00
1
@ Alguns não deveria. As funções não devem fechar sobre variáveis ​​que não estão usando internamente.
Plalx #
2
Ele pode ser acessado pela função vazia, mas não é, portanto, não há referências reais a ela, portanto deve ficar claro. A coleta de lixo acompanha as referências reais. Não é para se apegar a tudo o que poderia ter sido referenciado, apenas as coisas que são realmente referenciadas. Depois que o último f()é chamado, não há mais referências reais some. É inacessível e deve ser GCed.
precisa saber é o seguinte
1
@ jfriend00 Não consigo encontrar nada no (padrão) [ ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf] diz algo sobre apenas as variáveis ​​que ele usa internamente devem estar disponíveis. Na seção 13, a etapa de produção 4: Deixe o fechamento ser o resultado da criação de um novo objeto Function, conforme especificado em 13.2 , 10.2 "A referência do ambiente externo é usada para modelar o aninhamento lógico dos valores do Ambiente Lexical. A referência externa de um (interno ) Ambiente Lexical é uma referência ao Ambiente Lexical que envolve logicamente o Ambiente Lexical interno ".
algum
2
Bem, evalé um caso realmente especial. Por exemplo, evalnão pode ser alternativo ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… ), por exemplo var eval2 = eval. Se evalfor usado (e como não pode ser chamado por um nome diferente, é fácil), devemos assumir que ele pode usar qualquer coisa no escopo.
Paul Draper
49

Eu testei isso no IE9 + e no Firefox.

function f() {
  var some = [];
  while(some.length < 1e6) {
    some.push(some.length);
  }
  function g() { some; } //removing this fixes a massive memory leak
  return function() {};   //or removing this
}

var a = [];
var interval = setInterval(function() {
  var len = a.push(f());
  if(len >= 500) {
    clearInterval(interval);
  }
}, 10);

Site ao vivo aqui .

Eu esperava terminar com uma matriz de 500 function() {}'s, usando memória mínima.

Infelizmente, não foi esse o caso. Cada função vazia mantém uma matriz (para sempre inacessível, mas não GC'ed) de um milhão de números.

O Chrome finalmente pára e morre, o Firefox termina tudo depois de usar quase 4 GB de RAM, e o IE fica assintoticamente mais lento até mostrar "Memória insuficiente".

A remoção de qualquer uma das linhas comentadas corrige tudo.

Parece que todos esses três navegadores (Chrome, Firefox e IE) mantêm um registro de ambiente por contexto, não por fechamento. Boris supõe que a razão por trás dessa decisão é o desempenho, e isso parece provável, embora não tenha certeza do desempenho que pode ser chamado à luz do experimento acima.

Se for necessário referenciar o fechamento some(concedido, não o usei aqui, mas imagine que sim), se em vez de

function g() { some; }

eu uso

var g = (function(some) { return function() { some; }; )(some);

ele corrigirá os problemas de memória movendo o fechamento para um contexto diferente da minha outra função.

Isso tornará minha vida muito mais entediante.

PS Por curiosidade, tentei isso em Java (usando sua capacidade de definir classes dentro de funções). O GC funciona como eu esperava originalmente para Javascript.

Paul Draper
fonte
Eu acho que fechar parênteses perdidos para a função externa var g = (function (some) {return function () {some;};}) (some);
HCJ 14/11
15

As heurísticas variam, mas uma maneira comum de implementar esse tipo de coisa é criar um registro de ambiente para cada chamada f()no seu caso e armazenar apenas os locais fque estão realmente fechados (por algum fechamento) nesse registro de ambiente. Em seguida, qualquer fechamento criado na chamada para fmanter vivo o registro do ambiente. Acredito que é assim que o Firefox implementa fechamentos, pelo menos.

Isso tem os benefícios de acesso rápido a variáveis ​​fechadas e simplicidade de implementação. Ele tem a desvantagem do efeito observado, onde um fechamento de curta duração que fecha sobre algumas variáveis ​​faz com que seja mantido vivo por fechamentos de longa duração.

Pode-se tentar criar vários registros de ambiente para diferentes fechamentos, dependendo do que eles realmente fecham, mas isso pode ficar muito complicado muito rapidamente e pode causar problemas de desempenho e memória por conta própria ...

Boris Zbarsky
fonte
obrigado pela sua compreensão. Concluí que também é assim que o Chrome implementa fechamentos. Eu sempre pensei que eles foram implementados da última maneira, em que cada fechamento mantinha apenas o ambiente necessário, mas esse não é o caso. Gostaria de saber se é realmente tão complicado criar vários registros de ambiente. Em vez de agregar as referências dos fechamentos, aja como se cada um fosse o único fechamento. Eu achava que as considerações de desempenho eram o raciocínio aqui, embora as consequências de ter um registro de ambiente compartilhado parecesse ainda pior.
Paul Draper
A última maneira, em alguns casos, leva a uma explosão no número de registros do ambiente que precisam ser criados. A menos que você tente compartilhá-los entre as funções quando puder, mas precisará de um monte de máquinas complicadas para fazer isso. É possível, mas me disseram que as trocas de desempenho favorecem a abordagem atual.
Boris Zbarsky #
O número de registros é igual ao número de fechamentos criados. Eu poderia descrever O(n^2)ou O(2^n)como uma explosão, mas não um aumento proporcional.
Paul Draper
Bem, O (N) é uma explosão em comparação com O (1), especialmente quando cada um pode ocupar uma quantidade razoável de memória ... Novamente, eu não sou especialista nisso; perguntar no canal #jsapi em irc.mozilla.org provavelmente lhe dará uma explicação melhor e mais detalhada do que posso fornecer sobre quais são as desvantagens.
Boris Zbarsky
1
@ Esailija É realmente muito comum, infelizmente. Tudo o que você precisa é de um temporário grande na função (geralmente uma grande matriz digitada) que alguns retornos de chamada aleatórios de curta duração usam e um fechamento de longa duração. É chegar a um número de vezes recentemente para pessoas que escrevem aplicações web ...
Boris Zbarsky
0
  1. Manter estado entre chamadas de função Digamos que você tenha a função add () e gostaria que ela adicionasse todos os valores passados ​​a ele em várias chamadas e retorne a soma.

como add (5); // retorna 5

adicione (20); // retorna 25 (5 + 20)

adicione (3); // retorna 28 (25 + 3)

A maneira como você pode fazer isso primeiro é normal. Defina uma variável global. É claro que você pode usar uma variável global para armazenar o total. Mas lembre-se de que esse cara o comerá vivo se você (ab) usar globals.

agora mais recente maneira usando o fechamento sem definir variável global

(function(){

  var addFn = function addFn(){

    var total = 0;
    return function(val){
      total += val;
      return total;
    }

  };

  var add = addFn();

  console.log(add(5));
  console.log(add(20));
  console.log(add(3));
  
}());

Avinash Maurya
fonte
0

function Country(){
    console.log("makesure country call");	
   return function State(){
   
    var totalstate = 0;	
	
	if(totalstate==0){	
	
	console.log("makesure statecall");	
	return function(val){
      totalstate += val;	 
      console.log("hello:"+totalstate);
	   return totalstate;
    }	
	}else{
	 console.log("hey:"+totalstate);
	}
	 
  };  
};

var CA=Country();
 
 var ST=CA();
 ST(5); //we have add 5 state
 ST(6); //after few year we requare  have add new 6 state so total now 11
 ST(4);  // 15
 
 var CB=Country();
 var STB=CB();
 STB(5); //5
 STB(8); //13
 STB(3);  //16

 var CX=Country;
 var d=Country();
 console.log(CX);  //store as copy of country in CA
 console.log(d);  //store as return in country function in d

Avinash Maurya
fonte
por favor descreva a resposta
janith1024 12/12/18
0

(function(){

   function addFn(){

    var total = 0;
	
	if(total==0){	
	return function(val){
      total += val;	 
      console.log("hello:"+total);
	   return total+9;
    }	
	}else{
	 console.log("hey:"+total);
	}
	 
  };

   var add = addFn();
   console.log(add);  
   

    var r= add(5);  //5
	console.log("r:"+r); //14 
	var r= add(20);  //25
	console.log("r:"+r); //34
	var r= add(10);  //35
	console.log("r:"+r);  //44
	
	
var addB = addFn();
	 var r= addB(6);  //6
	 var r= addB(4);  //10
	  var r= addB(19);  //29
    
  
}());

Avinash Maurya
fonte