JavaScript kapanışları çöp nasıl toplanır


168

Kodumda birçok ciddi ve açık olmayan bellek sızıntısına yol açan aşağıdaki Chrome hatasını girdim :

(Bu sonuçlar, Chrome Dev Tools'un GC'yi çalıştıran ve daha sonra toplanmayan her şeyin yığın anlık görüntüsünü alan bellek profillerini kullanır .)

Aşağıdaki kodda, someClassörnek çöp toplanır (iyi):

var someClass = function() {};

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

window.f_ = f();

Ama bu durumda çöp toplanmayacak (kötü):

var someClass = function() {};

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

window.f_ = f();

Ve ilgili ekran görüntüsü:

Chromebug ekran görüntüsü

Bir kapama (bu durumda function() {}), nesneye aynı bağlamdaki başka bir kapama tarafından başvurulursa, bu kapamanın kendisine ulaşılabilir olsa da olmasa da, tüm nesneleri "canlı" tutar.

Benim sorum diğer tarayıcılarda (IE 9+ ve Firefox) çöp toplama hakkında. JavaScript yığın profiler gibi webkit araçlarına oldukça aşinayım, ancak diğer tarayıcıların araçlarını çok az biliyorum, bu yüzden bunu test edemedim.

Bu üç durumdan hangisinde IE9 + ve Firefox çöpü örneği toplar someClass ?


4
Deneyimsiz kullanıcılar için Chrome hangi değişkenlerin / nesnelerin çöp toplandığını ve ne zaman gerçekleştiğini test etmenize nasıl izin verir?
nnnnnn

1
Belki de konsol ona referans veriyor. Konsolu temizlediğinizde GC var mı?
david

1
@david Son örnekte unreachableişlev hiçbir zaman yürütülmez, böylece hiçbir şey günlüğe kaydedilmez.
James Montagne

1
Gerçeklerle yüz yüze gelsek bile, bu önemdeki bir hatanın yaşandığına inanmakta güçlük çekiyorum. Ancak tekrar tekrar koda bakıyorum ve başka bir mantıklı açıklama bulamıyorum. Kodları konsolda hiç çalıştırmamaya çalıştınız (tarayıcıyı yüklü bir komut dosyasından doğal olarak çalıştırmasına izin verin)?
plalx

1
bazı makaleleri daha önce okudum. "JavaScript uygulamalarında döngüsel başvuruları işleme" alt başlıklıdır, ancak JS / DOM döngüsel başvuruları ilgilendiren hiçbir modern tarayıcı için geçerli değildir. Kapanışlardan bahseder, ancak tüm örneklerde, söz konusu değişkenler hala program tarafından kullanılabilirdi.
Paul Draper

Yanıtlar:


78

Anlayabildiğim kadarıyla, bu bir hata değil, beklenen davranıştır.

Mozilla'nın Bellek yönetimi sayfasından : "2012'den itibaren tüm modern tarayıcılar bir işaretleme ve süpürme çöp toplayıcısı gönderiyor." "Sınırlama: nesnelerin açıkça erişilemez hale getirilmesi gerekir " .

Başarısız someolduğu örneklerinizde kapanışta hala erişilebilir. Erişilemez hale getirmek için iki yol denedim ve her ikisi de işe yaradı. Ya some=nullartık ihtiyaç duymadığınızı belirlersiniz, ya da siz belirlersiniz window.f_ = null;ve gider.

Güncelleme

Windows'ta Chrome 30, FF25, Opera 12 ve IE10'da denedim.

Standart çöp toplama hakkında bir şey söylemek, ama ne olacağına bazı ipuçları verir vermez.

  • Bölüm 13 İşlev tanımı, adım 4: "Kapatmanın, 13.2'de belirtildiği gibi yeni bir İşlev nesnesi oluşturmasının sonucu olmasına izin verin"
  • Kısım 13.2 "Kapsam tarafından belirtilen Sözlüksel Bir Ortam" (scope = closure)
  • Bölüm 10.2 Sözlüksel Ortamlar:

Msgstr "Bir (iç) Lexical Ortamının dış referansı, dahili Lexical Ortamını mantıksal olarak çevreleyen Lexical Environment'a bir referanstır.

Dışsal bir Lexical Environment elbette kendi dış Lexical Environment'a sahip olabilir. Bir Lexical Environment, birden fazla iç Lexical Ortamının dış ortamı olarak işlev görebilir. Bir Örneğin, Fonksiyon Bildirgesi iç içe geçmiş iki içeren Fonksiyon Açıklamasında sonra iç içe fonksiyonların her birinin Sözcük Ortamları dış Sözcük Çevre çevreleyen fonksiyonun mevcut yürütme Sözcük Çevre olarak sahip olacaktır."

Böylece, bir fonksiyonun ebeveynin ortamına erişimi olacaktır.

Bu nedenle, somegeri dönen fonksiyonun kapanmasında mevcut olmalıdır.

O zaman neden her zaman mevcut değil?

Chrome ve FF'nin bazı durumlarda değişkeni ortadan kaldıracak kadar akıllı olduğu görülüyor, ancak hem Opera hem de IE'de somedeğişken kapanmada mevcut (NB: bunu bir kesme noktasını görmek return nullve hata ayıklayıcıyı kontrol etmek için).

GC some, fonksiyonlarda kullanılıp kullanılmadığını tespit etmek için geliştirilebilir , ancak karmaşık olacaktır.

Kötü bir örnek:

var someClass = function() {};

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

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

Yukarıdaki örnekte GC değişkenin kullanılıp kullanılmadığını bilmenin bir yolu yoktur (kod test edildi ve Chrome30, FF25, Opera 12 ve IE10'da çalışıyor).

Nesneye yapılan başvuru, başka bir değer atanarak kesilirse bellek serbest bırakılır window.f_.

Bence bu bir hata değil.


4
Ancak, setTimeout()geri çağırma çalıştığında, geri çağrının bu işlev kapsamı setTimeout()yapılır ve bu kapsamın tümünün çöp toplanması gerekir some. Artık some, kapanıştaki örneğe erişebilen herhangi bir kod çalıştırılamaz . Çöp toplanmalıdır. Son örnek daha da kötüdür, çünkü unreachable()çağrılmamıştır ve kimsenin referansı yoktur. Kapsamı da GCed olmalıdır. Her ikisi de böcek gibi görünüyor. JS'de, bir işlev kapsamındaki şeyleri "serbest bırakmak" için herhangi bir dil gereksinimi yoktur.
jfriend00

1
@some Yapmamalı. Fonksiyonun dahili olarak kullanmadığı değişkenleri kapatması gerekmez.
plalx

2
Boş fonksiyon tarafından erişilebilir, ancak gerçek referanslar olmadığı için açık olmalıdır. Çöp toplama gerçek referansları takip eder. Referans verilebilecek her şeye tutunması gerekmiyor, sadece gerçekte atıfta bulunulan şeyler. Sonuncu f()çağrıldığında, artık gerçek referanslar yoktur some. Erişilemez ve GC olması gerekir.
jfriend00

1
@ jfriend00 (ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf ] içinde hiçbir şey bulamıyorum sadece dahili olarak kullandığı değişkenler hakkında bir şey mevcut olmalıdır diyor. Bölüm 13'te, üretim adımı 4: Kapanmanın , 13.2 , 10.2 " de belirtildiği gibi yeni bir Fonksiyon nesnesi oluşturmasının sonucu olmasına izin verin. " Dış ortam referansı, Lexical Environment değerlerinin mantıksal yuvalamasını modellemek için kullanılır. ) Lexical Environment, iç Lexical Ortamını mantıksal olarak çevreleyen Lexical Environment'a bir referanstır. "
bazı

2
Eh, evalgerçekten özel bir durumdur. Örneğin, diğer evalad ( ör . Developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… ), ör var eval2 = eval. Eğer evalkullanılan (ve bunu yapmak kolaydır farklı bir isim tarafından çağrılamaz beri), o zaman bu kapsamda her şeyi kullanabilirsiniz üstlenmelidir.
Paul Draper

49

Bunu IE9 + ve Firefox'ta test ettim.

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

Burada canlı site .

function() {}Minimum bellek kullanarak 500'lü bir dizi ile sarılmayı umdum .

Maalesef konu bu değildi. Her boş işlev bir milyon sayıdan oluşan (sonsuza kadar ulaşılamaz, ancak GC'ed değil) bir dizi üzerinde durur.

Chrome sonunda durur ve ölür, Firefox neredeyse 4GB RAM kullandıktan sonra her şeyi bitirir ve IE "Bellek yetersiz" gösterene kadar asimptotik olarak yavaşlar.

Yorum yapılan satırlardan birini kaldırmak her şeyi düzeltir.

Bu tarayıcıların üçünün (Chrome, Firefox ve IE) her kapatma için değil, içerik başına bir ortam kaydı tuttuğu görülmektedir. Boris, bu kararın arkasındaki sebebin performans olduğunu varsayıyor ve bu muhtemel görünüyor, ancak yukarıdaki deney ışığında nasıl performans gösterilebileceğinden emin değilim.

Bir kapatma referansına ihtiyaç varsa some(burada kullanmadım, ama hayal ettim)

function g() { some; }

kullanırım

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

kapatmayı diğer işlevimden farklı bir bağlama taşıyarak bellek sorunlarını düzeltir.

Bu hayatımı çok daha sıkıcı hale getirecek.

PS Merakla, bunu Java'da denedim (fonksiyonların içindeki sınıfları tanımlama yeteneğini kullanarak). GC aslında Javascript için umduğum gibi çalışıyor.


Dış fonksiyon için kaçırılan kapanış parantezinin var g = (function (some) {return function () {some;};}) (some);
HCJ

15

Sezgisel tarama çeşitlilik gösterir, ancak bu tür şeyleri uygulamanın yaygın bir yolu, davanızdaki her çağrı için bir ortam kaydı oluşturmaktır f()ve yalnızca o yerel kayıtlarını f( bazı kapanışlarla) o ortam kaydında depolamaktır . Sonra çağrıda oluşturulan herhangi bir kapatma fçevre kaydını canlı tutar. En azından Firefox'un kapanışları böyle uyguladığına inanıyorum.

Bunun, kapalı değişkenlere hızlı erişim ve uygulamanın basitliği gibi avantajları vardır. Gözlemlenen etkinin dezavantajı vardır, burada bazı değişkenler üzerinde kısa ömürlü bir kapanma, uzun ömürlü kapaklar tarafından canlı tutulmasına neden olur.

Gerçekte neyi kapattıklarına bağlı olarak farklı kapanışlar için birden fazla ortam kaydı oluşturmayı deneyebilir, ancak bu çok hızlı bir şekilde karmaşıklaşabilir ve kendi başına performans ve bellek sorunlarına neden olabilir ...


anlayışınız için teşekkür ederim. Sonuç olarak Chrome'un da kapakları nasıl uyguladığı sonucuna vardım. Her zaman, her bir kapağın sadece ihtiyaç duyduğu ortamı koruduğu ikinci şekilde uygulandığını düşündüm, ancak durum böyle değil. Birden fazla ortam kaydı oluşturmanın gerçekten bu kadar karmaşık olup olmadığını merak ediyorum. Kapakların referanslarını toplamak yerine, her biri tek kapakmış gibi davranın. Performans kaygısının burada akıl yürütme olduğunu tahmin etmiştim, ancak bana göre paylaşılan bir çevre kaydına sahip olmanın sonuçları daha da kötü görünüyor.
Paul Draper

İkinci durumda, bazı durumlarda yaratılması gereken çevre kayıtlarının sayısında bir patlama meydana gelir. Yapabildiğiniz zaman bunları işlevler arasında paylaşmaya çalışmıyorsanız, ancak bunu yapmak için bir dizi karmaşık makineye ihtiyacınız var. Bu mümkün, ancak performans dengesinin mevcut yaklaşımı desteklediği söylendi.
Boris Zbarsky

Kayıt sayısı, oluşturulan kapatma sayısına eşittir. Açıklanan olabilir O(n^2)ya da O(2^n)orantılı bir artış, bir patlama olarak değil.
Paul Draper

O (N), O (1) ile karşılaştırıldığında bir patlamadır, özellikle de her biri yeterli miktarda bellek alabiliyorsa ... Yine, bu konuda uzman değilim; irc.mozilla.org'daki #jsapi kanalını sormak, büyük olasılıkla, ödünleşmelerin ne olduğunu sağlayabileceğimden daha iyi ve daha ayrıntılı bir açıklama getirecektir.
Boris Zbarsky

1
@Esailija Maalesef aslında oldukça yaygın. İhtiyacınız olan tek şey, bazı rastgele kısa ömürlü geri arama kullanımı ve uzun ömürlü bir kapatma işlevinde büyük bir geçici (tipik olarak büyük bir dizi). Son zamanlarda web uygulamaları yazan insanlar için birkaç kez geldi ...
Boris Zbarsky

0
  1. İşlev çağrıları arasında Durumu Koru Diyelim ki, add () işlevine sahipsiniz ve birkaç çağrıda kendisine iletilen tüm değerleri eklemesini ve toplamı döndürmesini istiyorsunuz.

gibi ekle (5); // 5 döndürür

(20) ekleyin; // 25 döndürür (5 + 20)

(3) ekleyin; // 28 döndürür (25 + 3)

Bunu yapmanın iki yolu normaldir, global bir değişken tanımlamak Tabii ki, toplamı tutmak için global bir değişken kullanabilirsiniz. Ancak, eğer küreselleri kullanırsanız, bu dostun sizi canlı yiyeceğini unutmayın.

şimdi global değişkeni tanımlama ile kapatma kullanarak en son yolu

(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


lütfen cevabı açıklayın
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
    
  
}());

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.