Come vengono chiuse le chiusure JavaScript


168

Ho registrato il seguente bug di Chrome , che ha portato a molte perdite di memoria gravi e non ovvie nel mio codice:

(Questi risultati utilizzano il profiler di memoria di Chrome Dev Tools , che esegue il GC, e quindi acquisisce un'istantanea di heap di tutto ciò che non è stato raccolto e raccolto.)

Nel codice seguente, l' someClassistanza è garbage collection (buona):

var someClass = function() {};

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

window.f_ = f();

Ma non sarà spazzatura raccolta in questo caso (male):

var someClass = function() {};

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

window.f_ = f();

E lo screenshot corrispondente:

screenshot di Chromebug

Sembra che una chiusura (in questo caso function() {}) mantenga "vivi" tutti gli oggetti se l'oggetto è referenziato da qualsiasi altra chiusura nello stesso contesto, indipendentemente dal fatto che tale chiusura stessa sia o meno raggiungibile.

La mia domanda riguarda la garbage collection della chiusura in altri browser (IE 9+ e Firefox). Conosco abbastanza bene gli strumenti di webkit, come il profiler di heap JavaScript, ma conosco poco degli strumenti di altri browser, quindi non sono stato in grado di testarlo.

In quale di questi tre casi IE9 + e Firefox garbage raccolgono l' someClass istanza?


4
Per i non iniziati, in che modo Chrome ti consente di verificare quali variabili / oggetti vengono raccolti in modo inutile e quando ciò accade?
nnnnnn,

1
Forse la console sta mantenendo un riferimento ad essa. Viene GCed quando si cancella la console?
david

1
@david Nell'ultimo esempio la unreachablefunzione non viene mai eseguita, quindi non viene registrato nulla.
James Montagne,

1
Ho difficoltà a credere che sia passato un bug di tale importanza, anche se sembra che ci troviamo di fronte ai fatti. Comunque sto guardando il codice ancora e ancora e non trovo altre spiegazioni razionali. Hai provato a non eseguire affatto il codice nella console (ovvero lasciare che il browser lo esegua naturalmente da uno script caricato)?
Plalx,

1
@some, ho letto quell'articolo prima. È sottotitolato "Gestione dei riferimenti circolari nelle applicazioni JavaScript", ma la preoccupazione dei riferimenti circolari JS / DOM non si applica a nessun browser moderno. Menziona le chiusure, ma in tutti gli esempi, le variabili in questione erano ancora utilizzabili dal programma.
Paul Draper,

Risposte:


78

Per quanto ne so, questo non è un bug ma il comportamento previsto.

Dalla pagina di gestione della memoria di Mozilla : "A partire dal 2012, tutti i browser moderni lanciano un garbage collector". "Limitazione: gli oggetti devono essere resi esplicitamente irraggiungibili " .

Nei tuoi esempi in cui fallisce someè ancora raggiungibile nella chiusura. Ho provato due modi per renderlo irraggiungibile ed entrambi funzionano. O imposti some=nullquando non ti serve più, oppure imposti window.f_ = null;e sparirà.

Aggiornare

L'ho provato in Chrome 30, FF25, Opera 12 e IE10 su Windows.

Lo standard non dice nulla sulla raccolta dei rifiuti, ma fornisce alcuni indizi su cosa dovrebbe accadere.

  • Sezione 13 Definizione della funzione, passaggio 4: "Lascia che la chiusura sia il risultato della creazione di un nuovo oggetto Funzione come specificato in 13.2"
  • Sezione 13.2 "un ambiente lessicale specificato da Scope" (scope = chiusura)
  • Sezione 10.2 Ambienti lessicali:

"Il riferimento esterno di un ambiente lessicale (interno) è un riferimento all'ambiente lessicale che circonda logicamente l'ambiente lessicale interno.

Un ambiente lessicale esterno può ovviamente avere un proprio ambiente lessicale esterno. Un ambiente lessicale può fungere da ambiente esterno per più ambienti lessicali interni. Ad esempio, se una Dichiarazione di funzione contiene due Dichiarazioni di funzione nidificate, gli ambienti lessicali di ciascuna delle funzioni nidificate avranno come ambiente lessicale esterno l'ambiente lessicale dell'esecuzione corrente della funzione circostante. "

Quindi, una funzione avrà accesso all'ambiente del genitore.

Quindi, somedovrebbe essere disponibile nella chiusura della funzione di ritorno.

Allora perché non è sempre disponibile?

Sembra che Chrome e FF siano abbastanza intelligenti da eliminare la variabile in alcuni casi, ma sia in Opera che in IE la somevariabile è disponibile nella chiusura (NB: per visualizzare questo set attivo return nulle controllare il debugger).

Il GC potrebbe essere migliorato per rilevare se someviene utilizzato o meno nelle funzioni, ma sarà complicato.

Un cattivo esempio:

var someClass = function() {};

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

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

Nell'esempio sopra, il GC non ha modo di sapere se la variabile viene utilizzata o meno (codice testato e funziona in Chrome30, FF25, Opera 12 e IE10).

La memoria viene rilasciata se il riferimento all'oggetto viene interrotto assegnando un altro valore a window.f_.

Secondo me questo non è un bug.


4
Ma, una volta setTimeout()eseguito il setTimeout()callback, viene eseguito quell'ambito della funzione del callback e l'intero ambito deve essere garbage collection, rilasciando il suo riferimento some. Non è più possibile eseguire alcun codice in grado di raggiungere l'istanza di somenella chiusura. Dovrebbe essere spazzatura raccolta. L'ultimo esempio è anche peggio perché unreachable()non viene nemmeno chiamato e nessuno ha un riferimento ad esso. Il suo ambito dovrebbe essere anche GCed. Entrambi sembrano bug. In JS non è richiesto alcun linguaggio per "liberare" le cose nell'ambito di una funzione.
jfriend00,

1
@some Non dovrebbe. Le funzioni non dovrebbero chiudersi su variabili che non stanno usando internamente.
plalx,

2
È possibile accedervi con la funzione vuota, ma non è così non ci sono riferimenti reali ad essa, quindi dovrebbe essere chiaro. La garbage collection tiene traccia dei riferimenti effettivi. Non dovrebbe trattenere tutto ciò a cui si potrebbe fare riferimento, ma solo le cose a cui si fa effettivamente riferimento. Una volta f()chiamato l'ultimo , non ci sono più riferimenti reali a some. È irraggiungibile e dovrebbe essere GCed.
jfriend00,

1
@ jfriend00 Non riesco a trovare nulla nel (standard) [ ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf] dice che qualcosa che riguarda solo le variabili che utilizza internamente dovrebbe essere disponibile. Nella sezione 13, la fase di produzione 4: Consenti alla chiusura di essere il risultato della creazione di un nuovo oggetto Funzione come specificato in 13.2 , 10.2 "Il riferimento di ambiente esterno viene utilizzato per modellare l'annidamento logico dei valori di Ambiente lessicale. Il riferimento esterno di un (interno ) L'ambiente lessicale è un riferimento all'ambiente lessicale che circonda logicamente l'ambiente lessicale interno. "
un po '

2
Bene, evalè un caso davvero speciale. Ad esempio, evalnon può essere aliasato ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… ), ad es var eval2 = eval. Se evalviene usato (e poiché non può essere chiamato con un nome diverso, è facile da fare), allora dobbiamo presumere che possa usare qualsiasi cosa nell'ambito.
Paul Draper,

49

Ho provato questo in IE9 + e 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);

Sito live qui .

Speravo di finire con un array di 500 function() {}, usando una memoria minima.

Sfortunatamente, non era così. Ogni funzione vuota mantiene una matrice (sempre irraggiungibile, ma non GC) di un milione di numeri.

Chrome alla fine si arresta e muore, Firefox termina il tutto dopo aver usato quasi 4 GB di RAM e IE diventa asintoticamente più lento fino a quando non mostra "Memoria insufficiente".

La rimozione di una delle righe commentate risolve tutto.

Sembra che tutti e tre questi browser (Chrome, Firefox e IE) mantengano un record ambientale per contesto, non per chiusura. Boris ipotizza che il motivo alla base di questa decisione sia la performance, e questo sembra probabile, anche se non sono sicuro di come si possa chiamare performante alla luce dell'esperimento di cui sopra.

Se è necessario un riferimento di chiusura some(scontato, non l'ho usato qui, ma immagina di averlo fatto), se invece di

function g() { some; }

Io uso

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

risolverà i problemi di memoria spostando la chiusura in un contesto diverso rispetto alla mia altra funzione.

Questo renderà la mia vita molto più noiosa.

PS Per curiosità, l'ho provato in Java (usando la sua capacità di definire le classi all'interno delle funzioni). GC funziona come avevo inizialmente sperato in Javascript.


Penso che la parentesi di chiusura sia mancata per la funzione esterna var g = (function (some) {return function () {some;};}) (some);
HCJ,

15

L'euristica varia, ma un modo comune per implementare questo genere di cose è quello di creare un record di ambiente per ogni chiamata f()nel tuo caso e archiviare solo i locali fche sono effettivamente chiusi (da qualche chiusura) in quel record di ambiente. Quindi qualsiasi chiusura creata nella chiamata per fmantenere in vita il record dell'ambiente. Credo che questo sia il modo in cui Firefox implementa le chiusure, almeno.

Ciò ha i vantaggi di un rapido accesso alle variabili chiuse e della semplicità di implementazione. Ha lo svantaggio dell'effetto osservato, in cui una chiusura di breve durata che si chiude su alcune variabili fa sì che sia mantenuta in vita da chiusure di lunga durata.

Si potrebbe provare a creare più record di ambiente per diverse chiusure, a seconda di ciò che effettivamente chiudono, ma ciò può diventare molto complicato molto rapidamente e può causare problemi di prestazioni e memoria propri ...


grazie per la tua comprensione. Sono giunto a concludere che anche Chrome implementa le chiusure. Ho sempre pensato che fossero implementati in quest'ultimo modo, in cui ogni chiusura manteneva solo l'ambiente di cui aveva bisogno, ma non è così. Mi chiedo se sia davvero così complicato creare più record di ambiente. Piuttosto che aggregare i riferimenti delle chiusure, agire come se ognuna fosse l'unica chiusura. Avevo immaginato che le considerazioni sulle prestazioni fossero il ragionamento qui, anche se per me le conseguenze di avere un record di ambiente condiviso sembrano anche peggiori.
Paul Draper,

L'ultimo modo in alcuni casi porta a un'esplosione del numero di record ambientali che devono essere creati. A meno che tu non ti sforzi di condividerle tra le varie funzioni quando puoi, ma allora hai bisogno di un sacco di macchine complicate per farlo. È possibile, ma mi è stato detto che i compromessi prestazionali favoriscono l'approccio attuale.
Boris Zbarsky,

Il numero di record è uguale al numero di chiusure create. Potrei descriverlo O(n^2)o O(2^n)come un'esplosione, ma non un aumento proporzionale.
Paul Draper,

Bene, O (N) è un'esplosione rispetto a O (1), specialmente quando ognuno può occupare una buona quantità di memoria ... Ancora una volta, non sono un esperto in questo; chiedendo sul canale #jsapi su irc.mozilla.org è probabile che ti dia una spiegazione migliore e più dettagliata di quella che posso fornire su quali siano i compromessi.
Boris Zbarsky il

1
@Esailija In realtà è abbastanza comune, sfortunatamente. Tutto ciò di cui hai bisogno è un grande temporaneo nella funzione (in genere un array tipizzato di grandi dimensioni) utilizzato da un callback casuale di breve durata e una chiusura di lunga durata. Di recente è
successo

0
  1. Mantieni lo stato tra le chiamate di funzione Supponiamo che tu abbia la funzione add () e vorresti che aggiungesse tutti i valori passati in più chiamate e restituisse la somma.

come aggiungere (5); // restituisce 5

aggiungi (20); // restituisce 25 (5 + 20)

aggiungi (3); // restituisce 28 (25 + 3)

due modi per farlo prima è normale definire una variabile globale Naturalmente, è possibile utilizzare una variabile globale per contenere il totale. Ma tieni presente che questo tipo ti mangerà vivo se (ab) usi i globali.

ora il modo più recente di utilizzare la chiusura senza definire la variabile globale

(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));
  
}());


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


per favore descrivi la risposta
janith1024,

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
    
  
}());

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.