Bagaimana penutupan JavaScript dikumpulkan dari sampah

168

Saya telah mencatat bug Chrome berikut , yang menyebabkan banyak kebocoran memori yang serius dan tidak jelas dalam kode saya:

(Hasil ini menggunakan profil memori Chrome Dev Tools ' , yang menjalankan GC, dan kemudian mengambil snapshot tumpukan dari segala sesuatu yang tidak dikumpulkan dikumpulkan.)

Dalam kode di bawah ini, someClasscontohnya adalah sampah yang dikumpulkan (baik):

var someClass = function() {};

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

window.f_ = f();

Tapi itu tidak akan menjadi sampah yang dikumpulkan dalam kasus ini (buruk):

var someClass = function() {};

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

window.f_ = f();

Dan tangkapan layar yang sesuai:

tangkapan layar Chromebug

Tampaknya penutupan (dalam hal ini, function() {}) membuat semua benda "hidup" jika objek direferensikan oleh penutupan lain dalam konteks yang sama, apakah penutupan itu sendiri bahkan dapat dijangkau.

Pertanyaan saya adalah tentang pengumpulan penutupan sampah di browser lain (IE 9+ dan Firefox). Saya cukup akrab dengan alat-alat webkit, seperti JavaScript heap profiler, tapi saya tahu sedikit alat peramban lain, jadi saya belum bisa mengujinya.

Di mana dari tiga kasus ini akankah sampah IE9 + dan Firefox mengumpulkan someClass contoh?

Paul Draper
sumber
4
Untuk yang belum tahu, bagaimana Chrome membiarkan Anda menguji variabel / objek mana yang dikumpulkan, dan kapan itu terjadi?
nnnnnn
1
Mungkin konsol menyimpan referensi untuk itu. Apakah itu mendapatkan GCed saat Anda menghapus konsol?
david
1
@david Dalam contoh terakhir unreachablefungsi tidak pernah dieksekusi sehingga tidak ada yang benar-benar dicatat.
James Montagne
1
Saya mengalami kesulitan untuk percaya bahwa bug yang penting itu telah melalui, bahkan jika kita tampaknya dihadapkan dengan fakta. Namun saya melihat kode lagi dan lagi dan saya tidak menemukan penjelasan rasional lainnya. Anda mencoba untuk tidak menjalankan kode di konsol sama sekali (alias biarkan browser menjalankannya secara alami dari skrip yang dimuat)?
plalx
1
@Beberapa, saya pernah membaca artikel itu sebelumnya. Subtitle ini berjudul "Menangani referensi melingkar dalam aplikasi JavaScript", tetapi kekhawatiran referensi melingkar JS / DOM tidak berlaku untuk browser modern. Ini menyebutkan penutupan, tetapi dalam semua contoh, variabel yang dipertanyakan masih mungkin digunakan oleh program.
Paul Draper

Jawaban:

78

Sejauh yang saya tahu, ini bukan bug tetapi perilaku yang diharapkan.

Dari halaman Manajemen memori Mozilla : "Pada 2012, semua browser modern mengirimkan pengumpul sampah tanda dan sapuan." "Batasan: objek harus dibuat secara eksplisit tidak dapat dijangkau " .

Dalam contoh Anda di mana kegagalan someitu masih dapat dijangkau di penutupan. Saya mencoba dua cara untuk membuatnya tidak terjangkau dan keduanya berfungsi. Entah Anda mengatur some=nullkapan Anda tidak membutuhkannya lagi, atau Anda menetapkan window.f_ = null;dan itu akan hilang.

Memperbarui

Saya sudah mencobanya di Chrome 30, FF25, Opera 12 dan IE10 di Windows.

The standar tidak mengatakan apa-apa tentang pengumpulan sampah, tetapi memberikan beberapa petunjuk tentang apa yang harus terjadi.

  • Bagian 13 Definisi fungsi, langkah 4: "Biarkan penutup menjadi hasil dari menciptakan objek Fungsi baru sebagaimana ditentukan dalam 13.2"
  • Bagian 13.2 "Lingkungan Leksikal yang ditentukan oleh Lingkup" (scope = closure)
  • Bagian 10.2 Lingkungan Leksikal:

"Referensi luar Lingkungan Leksikal (dalam) adalah referensi ke Lingkungan Leksikal yang secara logis mengelilingi Lingkungan Leksikal dalam.

Lingkungan Leksikal Luar mungkin, tentu saja, memiliki Lingkungan Leksikal luarnya sendiri. Lingkungan Leksikal dapat berfungsi sebagai lingkungan luar untuk beberapa Lingkungan Leksikal dalam. Misalnya, jika Deklarasi Fungsi berisi dua Deklarasi Fungsi bersarang maka Lingkungan Leksikal dari masing-masing fungsi bersarang akan memiliki Lingkungan Leksikal luarnya, Lingkungan Leksikal dari pelaksanaan fungsi sekitarnya saat ini. "

Jadi, suatu fungsi akan memiliki akses ke lingkungan induknya.

Jadi, someharus tersedia di penutupan fungsi kembali.

Lalu mengapa tidak selalu tersedia?

Tampaknya Chrome dan FF cukup pintar untuk menghilangkan variabel dalam beberapa kasus, tetapi di Opera dan IE, somevariabel tersedia di penutupan (NB: untuk melihat ini atur breakpoint return nulldan periksa debugger).

GC dapat ditingkatkan untuk mendeteksi apakah somedigunakan atau tidak dalam fungsi, tetapi akan rumit.

Contoh buruk:

var someClass = function() {};

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

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

Dalam contoh di atas GC tidak memiliki cara untuk mengetahui apakah variabel tersebut digunakan atau tidak (kode diuji dan berfungsi di Chrome30, FF25, Opera 12 dan IE10).

Memori dilepaskan jika referensi ke objek rusak dengan menetapkan nilai lain window.f_.

Menurut saya ini bukan bug.

beberapa
sumber
4
Tetapi, begitu setTimeout()callback berjalan, fungsi lingkup setTimeout()callback dilakukan dan seluruh lingkup harus dikumpulkan, melepaskan referensi ke some. Tidak ada lagi kode yang dapat dijalankan yang dapat mencapai instance somedalam penutupan. Itu harus menjadi sampah yang dikumpulkan. Contoh terakhir bahkan lebih buruk karena unreachable()bahkan tidak dipanggil dan tidak ada yang memiliki referensi untuk itu. Lingkupnya juga harus GCed. Keduanya tampak seperti bug. Tidak ada persyaratan bahasa di JS untuk "membebaskan" hal-hal dalam lingkup fungsi.
jfriend00
1
@Beberapa Seharusnya tidak. Fungsi tidak seharusnya menutup variabel yang tidak mereka gunakan secara internal.
plalx
2
Itu dapat diakses oleh fungsi kosong, tetapi tidak jadi tidak ada referensi aktual untuk itu sehingga harus jelas. Pengumpulan sampah melacak referensi aktual. Seharusnya tidak memegang semua yang bisa dirujuk, hanya hal-hal yang benar-benar dirujuk. Setelah yang terakhir f()dipanggil, tidak ada referensi aktual somelagi. Itu tidak bisa dijangkau dan harus GCed.
jfriend00
1
@ jfriend00 Saya tidak dapat menemukan apa pun di (standar) [ ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf] mengatakan apa pun tentang hanya variabel yang digunakan secara internal yang harus tersedia. Pada bagian 13, langkah produksi 4: Biarkan penutup menjadi hasil dari menciptakan objek Fungsi baru sebagaimana ditentukan dalam 13.2 , 10.2 "Referensi lingkungan luar digunakan untuk memodelkan bersarang logis dari nilai-nilai Lingkungan Lexical. Referensi luar dari (dalam ) Lexical Environment adalah referensi ke Lingkungan Lexical yang secara logis mengelilingi Lingkungan Lexical batin. "
beberapa
2
Nah, evalini kasus yang sangat spesial. Misalnya, evaltidak bisa alias ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… ), mis var eval2 = eval. Jika evaldigunakan (dan karena itu tidak dapat dipanggil dengan nama yang berbeda, itu mudah dilakukan), maka kita harus berasumsi bahwa itu dapat menggunakan apa pun dalam ruang lingkup.
Paul Draper
49

Saya menguji ini di IE9 + dan 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);

Situs langsung di sini .

Saya berharap untuk berakhir dengan array 500 function() {}'s, menggunakan memori minimal.

Sayangnya, bukan itu masalahnya. Setiap fungsi kosong berpegang pada array sejuta angka (tidak dapat dijangkau, tetapi tidak GC'ed).

Chrome akhirnya berhenti dan mati, Firefox menyelesaikan semuanya setelah menggunakan hampir 4GB RAM, dan IE tumbuh lebih lambat secara asimptot hingga menunjukkan "Kehabisan memori".

Menghapus salah satu dari baris yang dikomentari akan memperbaiki semuanya.

Tampaknya ketiga peramban ini (Chrome, Firefox, dan IE) menyimpan catatan lingkungan per konteks, bukan per penutupan. Boris berhipotesis bahwa alasan di balik keputusan ini adalah kinerja, dan itu tampaknya mungkin, meskipun saya tidak yakin bagaimana performannya dapat disebut dalam terang percobaan di atas.

Jika perlu referensi penutupan some(diberikan saya tidak menggunakannya di sini, tapi bayangkan saya lakukan), jika bukan

function g() { some; }

saya menggunakan

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

itu akan memperbaiki masalah memori dengan memindahkan penutup ke konteks yang berbeda dari fungsi saya yang lain.

Ini akan membuat hidup saya jauh lebih membosankan.

PS Karena penasaran, saya mencoba ini di Jawa (menggunakan kemampuannya untuk mendefinisikan kelas di dalam fungsi). GC berfungsi seperti yang semula saya harapkan untuk Javascript.

Paul Draper
sumber
Saya pikir kurung penutup tidak terjawab untuk fungsi luar var g = (function (some) {function return () {some;};}) (some);
HCJ
15

Heuristik berbeda-beda, tetapi cara umum untuk menerapkan hal semacam ini adalah dengan membuat catatan lingkungan untuk setiap panggilan ke f()dalam kasus Anda, dan hanya menyimpan penduduk lokal fyang benar-benar ditutup (dengan beberapa penutupan) dalam catatan lingkungan tersebut. Lalu setiap penutupan dibuat dalam panggilan kef menjaga catatan lingkungan hidup. Saya percaya ini adalah bagaimana Firefox mengimplementasikan penutupan, setidaknya.

Ini memiliki manfaat akses cepat ke variabel tertutup dan kesederhanaan implementasi. Ini memiliki kelemahan dari efek yang diamati, di mana penutupan jangka pendek menutup beberapa variabel menyebabkannya tetap hidup dengan penutupan berumur panjang.

Seseorang dapat mencoba membuat beberapa catatan lingkungan untuk penutupan yang berbeda, tergantung pada apa yang sebenarnya mereka tutup, tetapi itu bisa menjadi sangat rumit dengan sangat cepat dan dapat menyebabkan masalah kinerja dan memori ...

Boris Zbarsky
sumber
terima kasih atas wawasan anda. Saya berkesimpulan bahwa Chrome juga menerapkan penutupan. Saya selalu berpikir mereka diterapkan dengan cara yang terakhir, di mana setiap penutupan hanya menyimpan lingkungan yang dibutuhkan, tetapi tidak demikian halnya. Saya bertanya-tanya apakah benar-benar rumit untuk membuat beberapa catatan lingkungan. Daripada mengagregasi referensi penutupan, bertindaklah seolah-olah masing-masing adalah satu-satunya penutupan. Saya telah menduga bahwa pertimbangan kinerja adalah alasan di sini, meskipun bagi saya konsekuensi dari memiliki catatan lingkungan bersama tampak lebih buruk.
Paul Draper
Cara terakhir dalam beberapa kasus mengarah ke ledakan dalam jumlah catatan lingkungan yang perlu dibuat. Kecuali jika Anda berusaha keras untuk membaginya di seluruh fungsi saat Anda bisa, tetapi kemudian Anda membutuhkan banyak mesin yang rumit untuk melakukan itu. Itu mungkin, tapi saya diberitahu pengorbanan kinerja mendukung pendekatan saat ini.
Boris Zbarsky
Jumlah catatan sama dengan jumlah penutupan yang dibuat. Saya mungkin menggambarkan O(n^2)atau O(2^n)sebagai ledakan, tetapi bukan peningkatan yang proporsional.
Paul Draper
Nah, O (N) adalah ledakan dibandingkan dengan O (1), terutama ketika masing-masing dapat mengambil jumlah memori yang cukup ... Sekali lagi, saya bukan ahli dalam hal ini; bertanya di saluran #jsapi di irc.mozilla.org kemungkinan akan memberi Anda penjelasan yang lebih baik dan lebih rinci daripada yang bisa saya berikan tentang apa yang menjadi pengorbanannya.
Boris Zbarsky
1
@Esailija Sebenarnya cukup umum, sayangnya. Yang Anda butuhkan adalah fungsi sementara yang besar (biasanya array yang diketik besar) yang digunakan beberapa panggilan balik jangka pendek acak dan penutupan berumur panjang. Ini muncul beberapa kali baru-baru ini untuk orang yang menulis aplikasi web ...
Boris Zbarsky
0
  1. Pertahankan Status antara panggilan fungsi. Katakanlah Anda memiliki fungsi add () dan Anda ingin menambahkan semua nilai yang diteruskan ke dalam beberapa panggilan dan mengembalikan jumlahnya.

seperti add (5); // mengembalikan 5

tambah (20); // mengembalikan 25 (5 + 20)

tambahkan (3); // mengembalikan 28 (25 + 3)

dua cara Anda dapat melakukan ini pertama adalah mendefinisikan variabel global. Tentu saja, Anda dapat menggunakan variabel global untuk menahan total. Tetapi perlu diingat bahwa pria ini akan memakan Anda hidup-hidup jika Anda (ab) menggunakan global.

sekarang cara terbaru menggunakan penutupan tanpa mendefinisikan variabel 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
sumber
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
sumber
tolong jelaskan jawabannya
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
sumber