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 someClass
instancja 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:
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?
unreachable
funkcja nigdy nie jest wykonywana, więc w rzeczywistości nic nie jest rejestrowane.Odpowiedzi:
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,
some
jest nadal osiągalne w zamknięciu. Wypróbowałem dwa sposoby, aby uczynić to nieosiągalnym i oba działają. Albo ustawisz,some=null
kiedy już go nie potrzebujesz, albo ustawiszwindow.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ć.
Zatem funkcja będzie miała dostęp do środowiska rodzica.
Więc
some
powinien 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
some
zmienna jest dostępna w zamknięciu (uwaga: aby wyświetlić tęreturn null
opcję, ustaw punkt przerwania i sprawdź debugger).GC można ulepszyć, aby wykrywał, czy
some
jest używany, czy nie w funkcjach, ale będzie to skomplikowane.Zły przykład:
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.
źródło
setTimeout()
wywołania zwrotnego ten zakres funkcjisetTimeout()
wywołania zwrotnego jest wykonywany i cały ten zakres powinien zostać wyczyszczony, uwalniając odwołanie dosome
. Nie ma już żadnego kodu, który można uruchomić, który mógłby dotrzeć do wystąpieniasome
w 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.f()
wywołaniu ostatniego nie ma już żadnych odniesień do niegosome
. Jest nieosiągalny i powinien być GCed.eval
to naprawdę wyjątkowy przypadek. Na przykładeval
nie może mieć aliasu ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… ), npvar eval2 = eval
. Jeślieval
jest 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.Przetestowałem to w IE9 + i Firefox.
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 zamiastużywam
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.
źródło
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,f
które są faktycznie zamknięte (przez jakieś zamknięcie) w tym rekordzie środowiska. Następnie każde zamknięcie utworzone w wywołaniu w celuf
utrzymania 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ą ...
źródło
O(n^2)
lubO(2^n)
jako eksplozję, ale nie proporcjonalny wzrost.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
źródło
źródło
źródło