Jak zamknięcia JavaScript są zbierane bezużytecznie

168

Zarejestrowałem następujący błąd przeglądarki Chrome , który doprowadził do wielu poważnych i nieoczywistych wycieków pamięci w moim kodzie:

(Te wyniki wykorzystują profiler pamięci Chrome Dev Tools , który uruchamia GC, a następnie tworzy migawkę sterty wszystkiego, co nie zostało zebrane).

W poniższym kodzie someClassinstancja jest odśmiecana (dobra):

var someClass = function() {};

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

window.f_ = f();

Ale w tym przypadku nie będzie to śmieci (źle):

var someClass = function() {};

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

window.f_ = f();

I odpowiedni zrzut ekranu:

zrzut ekranu Chromebuga

Wydaje się, że zamknięcie (w tym przypadku function() {}) utrzymuje wszystkie obiekty „przy życiu”, jeśli do obiektu odwołuje się jakiekolwiek inne zamknięcie w tym samym kontekście, niezależnie od tego, czy samo zamknięcie jest osiągalne, czy nie.

Moje pytanie dotyczy usuwania śmieci z zamykania w innych przeglądarkach (IE 9+ i Firefox). Jestem dość zaznajomiony z narzędziami webkit, takimi jak profiler stosu JavaScript, ale niewiele wiem o narzędziach innych przeglądarek, więc nie byłem w stanie tego przetestować.

W którym z tych trzech przypadków IE9 + i Firefox będą zbierać śmieci z someClass instancji?

Paul Draper
źródło
4
Dla niewtajemniczonych, w jaki sposób Chrome pozwala przetestować, które zmienne / obiekty są zbierane jako śmieci i kiedy to się dzieje?
nnnnnn
1
Może konsola zachowuje do niego odniesienie. Czy dostaje GCed po wyczyszczeniu konsoli?
david
1
@david W ostatnim przykładzie unreachablefunkcja nigdy nie jest wykonywana, więc w rzeczywistości nic nie jest rejestrowane.
James Montagne
1
Trudno mi uwierzyć, że przeszedł tak ważny błąd, nawet jeśli wydaje się, że mamy do czynienia z faktami. Jednak ciągle patrzę na kod i nie znajduję żadnego innego racjonalnego wyjaśnienia. Próbowałeś w ogóle nie uruchamiać kodu w konsoli (czyli pozwolić przeglądarce uruchomić go naturalnie z załadowanego skryptu)?
plalx
1
@niektóre, czytałem już ten artykuł. Ma podtytuł „Obsługa odwołań cyklicznych w aplikacjach JavaScript”, ale problem odwołań cyklicznych JS / DOM nie dotyczy żadnej nowoczesnej przeglądarki. Wspomina o domknięciach, ale we wszystkich przykładach omawiane zmienne były nadal możliwe do wykorzystania przez program.
Paul Draper,

Odpowiedzi:

78

O ile wiem, nie jest to błąd, ale oczekiwane zachowanie.

Ze strony zarządzania pamięcią Mozilli : „Od 2012 r. Wszystkie współczesne przeglądarki udostępniają zbieracz śmieci typu„ mark and sweep ”. „Ograniczenie: obiekty muszą być wyraźnie nieosiągalne .

W twoich przykładach, gdzie zawodzi, somejest nadal osiągalne w zamknięciu. Wypróbowałem dwa sposoby, aby uczynić to nieosiągalnym i oba działają. Albo ustawisz, some=nullkiedy już go nie potrzebujesz, albo ustawisz window.f_ = null;i to zniknie.

Aktualizacja

Wypróbowałem to w Chrome 30, FF25, Opera 12 i IE10 w systemie Windows.

Norma nie mówi nic na temat zbierania śmieci, ale daje pewne wskazówki, co powinno się zdarzyć.

  • Sekcja 13 Definicja funkcji, krok 4: „Niech zamknięcie będzie wynikiem utworzenia nowego obiektu funkcji, jak określono w 13.2”
  • Sekcja 13.2 „Środowisko leksykalne określone przez zakres” (zakres = zamknięcie)
  • Sekcja 10.2 Środowiska leksykalne:

„Zewnętrzne odniesienie do (wewnętrznego) środowiska leksykalnego jest odniesieniem do środowiska leksykalnego, które logicznie otacza wewnętrzne środowisko leksykalne.

Zewnętrzne środowisko leksykalne może oczywiście mieć swoje własne zewnętrzne środowisko leksykalne. Środowisko leksykalne może służyć jako środowisko zewnętrzne dla wielu wewnętrznych środowisk leksykalnych. Na przykład, jeśli deklaracja funkcji zawiera dwie zagnieżdżone deklaracje funkcji, wówczas środowiska leksykalne każdej z zagnieżdżonych funkcji będą miały jako zewnętrzne środowisko leksykalne środowisko leksykalne bieżącego wykonania otaczającej funkcji. "

Zatem funkcja będzie miała dostęp do środowiska rodzica.

Więc somepowinien być dostępny w zamknięciu funkcji zwracającej.

Więc dlaczego nie jest zawsze dostępny?

Wygląda na to, że Chrome i FF są wystarczająco sprytne, aby w niektórych przypadkach wyeliminować zmienną, ale zarówno w Operze, jak i IE somezmienna jest dostępna w zamknięciu (uwaga: aby wyświetlić tę return nullopcję, ustaw punkt przerwania i sprawdź debugger).

GC można ulepszyć, aby wykrywał, czy somejest używany, czy nie w funkcjach, ale będzie to skomplikowane.

Zły przykład:

var someClass = function() {};

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

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

W powyższym przykładzie GC nie ma możliwości sprawdzenia, czy zmienna jest używana, czy nie (kod przetestowany i działa w Chrome30, FF25, Opera 12 i IE10).

Pamięć jest zwalniana, jeśli odwołanie do obiektu zostanie zerwane przez przypisanie innej wartości do window.f_.

Moim zdaniem to nie jest błąd.

trochę
źródło
4
Ale po uruchomieniu setTimeout()wywołania zwrotnego ten zakres funkcji setTimeout()wywołania zwrotnego jest wykonywany i cały ten zakres powinien zostać wyczyszczony, uwalniając odwołanie do some. Nie ma już żadnego kodu, który można uruchomić, który mógłby dotrzeć do wystąpienia somew zamknięciu. Powinien być zebrany jako śmieci. Ostatni przykład jest jeszcze gorszy, ponieważ unreachable()nie jest nawet wywołany i nikt nie ma do niego odniesienia. Jego zakres również powinien zostać poddany GC. Te oba wydają się być błędami. W JS nie ma wymagania językowego, aby „zwolnić” rzeczy w zakresie funkcji.
jfriend00
1
@niektóre Nie powinno. Funkcje nie powinny zamykać zmiennych, których nie używają wewnętrznie.
plalx
2
Można uzyskać do niego dostęp przez pustą funkcję, ale tak nie jest, więc nie ma do niego rzeczywistych odniesień, więc powinno być jasne. Funkcja czyszczenia pamięci śledzi rzeczywiste odwołania. Nie powinno obejmować wszystkiego, do czego można się odwoływać, tylko rzeczy, do których się odnoszą. Po f()wywołaniu ostatniego nie ma już żadnych odniesień do niego some. Jest nieosiągalny i powinien być GCed.
jfriend00
1
@ jfriend00 Nie mogę znaleźć niczego w (standardowym) [ ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf] mówi cokolwiek o tym, że powinny być dostępne tylko zmienne, których używa wewnętrznie. W sekcji 13, etap produkcji 4: Niech zamknięcie będzie wynikiem utworzenia nowego obiektu funkcji, jak określono w 13.2 , 10.2 "Odniesienie do środowiska zewnętrznego jest używane do modelowania logicznego zagnieżdżenia wartości środowiska leksykalnego. Odniesienie zewnętrzne a (wewnętrzne ) Środowisko leksykalne to odniesienie do środowiska leksykalnego, które w logiczny sposób otacza wewnętrzne środowisko leksykalne ”.
jakiś
2
Cóż, evalto naprawdę wyjątkowy przypadek. Na przykład evalnie może mieć aliasu ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… ), np var eval2 = eval. Jeśli evaljest używany (a ponieważ nie można go wywołać inną nazwą, jest to łatwe), musimy założyć, że może używać wszystkiego w zakresie.
Paul Draper
49

Przetestowałem to w IE9 + i 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);

Witryna na żywo tutaj .

Miałem nadzieję, że uda mi się skończyć z tablicą 500 function() {}przy minimalnym zużyciu pamięci.

Niestety tak się nie stało. Każda pusta funkcja zachowuje (wiecznie nieosiągalną, ale nie oznaczoną metodą GC) tablicę miliona liczb.

Chrome ostatecznie zatrzymuje się i umiera, Firefox kończy całą operację po wykorzystaniu prawie 4 GB pamięci RAM, a IE rośnie asymptotycznie wolniej, aż do wyświetlenia komunikatu „Brak pamięci”.

Usunięcie jednej z skomentowanych linii naprawia wszystko.

Wygląda na to, że wszystkie trzy przeglądarki (Chrome, Firefox i IE) przechowują zapis środowiska według kontekstu, a nie zamknięcia. Boris wysuwa hipotezę, że powodem tej decyzji są wyniki i wydaje się to prawdopodobne, chociaż nie jestem pewien, jak wydajne można je nazwać w świetle powyższego eksperymentu.

Jeśli zajdzie taka potrzeba, odwołanie do zamknięcia some(przyznane, że nie użyłem go tutaj, ale wyobraź sobie, że to zrobiłem), jeśli zamiast

function g() { some; }

używam

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

rozwiąże problemy z pamięcią, przenosząc zamknięcie do innego kontekstu niż moja inna funkcja.

Dzięki temu moje życie stanie się o wiele bardziej nudne.

PS Z ciekawości spróbowałem tego w Javie (wykorzystując jej zdolność do definiowania klas wewnątrz funkcji). GC działa tak, jak początkowo liczyłem na Javascript.

Paul Draper
źródło
Myślę, że brak nawiasu zamykającego dla funkcji zewnętrznej var g = (function (some) {return function () {some;};}) (some);
HCJ
15

Heurystyki są różne, ale powszechnym sposobem implementacji tego typu rzeczy jest utworzenie rekordu środowiska dla każdego wywołania f()w twoim przypadku i przechowywanie tylko tych miejsc, fktóre są faktycznie zamknięte (przez jakieś zamknięcie) w tym rekordzie środowiska. Następnie każde zamknięcie utworzone w wywołaniu w celu futrzymania przy życiu rekordu środowiska. Uważam, że przynajmniej w ten sposób Firefox implementuje zamknięcia.

Ma to zalety szybkiego dostępu do zamkniętych zmiennych i prostoty implementacji. Ma tę wadę, że obserwowany efekt polega na tym, że krótkotrwałe zamknięcie, zamykające się nad jakąś zmienną, powoduje, że jest ona utrzymywana przy życiu przez długotrwałe zamknięcia.

Można spróbować utworzyć wiele rekordów środowiska dla różnych zamknięć, w zależności od tego, co faktycznie zamykają, ale może to bardzo szybko się skomplikować i może powodować problemy z wydajnością i pamięcią ...

Boris Zbarsky
źródło
dziękuję za wgląd. Doszedłem do wniosku, że w ten sposób również Chrome implementuje zamknięcia. Zawsze myślałem, że są one realizowane w ten drugi sposób, w którym każde zamknięcie zachowuje tylko potrzebne środowisko, ale tak nie jest. Zastanawiam się, czy tworzenie wielu rekordów środowiska jest naprawdę takie skomplikowane. Zamiast gromadzić odniesienia do zamknięć, zachowuj się tak, jakby każde z nich było jedynym zamknięciem. Domyślałem się, że powodem tego były względy wydajności, chociaż wydaje mi się, że konsekwencje posiadania wspólnego środowiska wydają się jeszcze gorsze.
Paul Draper
Ten ostatni sposób w niektórych przypadkach prowadzi do eksplozji liczby rekordów środowiskowych, które należy utworzyć. Chyba że będziesz się bardzo starał udostępniać je między funkcjami, kiedy tylko możesz, ale wtedy potrzebujesz do tego zestawu skomplikowanych maszyn. Jest to możliwe, ale powiedziano mi, że kompromisy w zakresie wydajności sprzyjają obecnemu podejściu.
Boris Zbarsky
Liczba rekordów jest równa liczbie utworzonych zamknięć. Mógłbym opisać O(n^2)lub O(2^n)jako eksplozję, ale nie proporcjonalny wzrost.
Paul Draper
Cóż, O (N) jest eksplozją w porównaniu do O (1), zwłaszcza gdy każdy może zająć sporą ilość pamięci ... Ponownie, nie jestem w tym ekspertem; pytanie na kanale #jsapi na irc.mozilla.org prawdopodobnie da ci lepsze i bardziej szczegółowe wyjaśnienie, jakie są kompromisy niż ja.
Boris Zbarsky
1
@Esailija Niestety jest to dość powszechne. Wszystko, czego potrzebujesz, to duża zmienna tymczasowa w funkcji (zazwyczaj duża tablica wpisana), której używa niektóre losowe krótkotrwałe wywołania zwrotne i długotrwałe zamknięcie. Ostatnio wiele razy
zdarzało się to
0
  1. Zachowaj stan między wywołaniami funkcji Powiedzmy, że masz funkcję add () i chcesz, aby dodała wszystkie wartości przekazane do niej w kilku wywołaniach i zwróciła sumę.

jak add (5); // zwraca 5

dodaj (20); // zwraca 25 (5 + 20)

dodaj (3); // zwraca 28 (25 + 3)

na dwa sposoby możesz to zrobić. Najpierw jest normalne zdefiniowanie zmiennej globalnej Oczywiście możesz użyć zmiennej globalnej, aby zachować sumę. Ale pamiętaj, że ten koleś zje cię żywcem, jeśli (nad) użyjesz globali.

teraz najnowszy sposób używania domknięcia bez definiowania zmiennej globalnej

(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
źródło
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
źródło
proszę opisać odpowiedź
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
    
  
}());

Avinash Maurya
źródło