Cách đóng JavaScript được thu gom rác


168

Tôi đã ghi lại lỗi Chrome sau đây , dẫn đến nhiều rò rỉ bộ nhớ nghiêm trọng và không rõ ràng trong mã của tôi:

(Các kết quả này sử dụng trình lược tả bộ nhớ của Chrome Dev Tools , chạy GC, và sau đó chụp ảnh toàn bộ mọi thứ không được thu thập.)

Trong mã dưới đây, someClassví dụ là rác được thu thập (tốt):

var someClass = function() {};

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

window.f_ = f();

Nhưng nó sẽ không phải là rác được thu thập trong trường hợp này (xấu):

var someClass = function() {};

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

window.f_ = f();

Và ảnh chụp màn hình tương ứng:

ảnh chụp màn hình của Chromebug

Dường như một bao đóng (trong trường hợp này function() {}) giữ cho tất cả các đối tượng "sống" nếu đối tượng được tham chiếu bởi bất kỳ bao đóng nào khác trong cùng một bối cảnh, cho dù chính nó có thể truy cập được hay không.

Câu hỏi của tôi là về bộ sưu tập rác đóng cửa trong các trình duyệt khác (IE 9+ và Firefox). Tôi khá quen thuộc với các công cụ của webkit, chẳng hạn như trình lược tả heap JavaScript, nhưng tôi biết rất ít các công cụ của các trình duyệt khác, vì vậy tôi chưa thể kiểm tra điều này.

Trong trường hợp nào trong ba trường hợp này, IE9 + và Firefox sẽ thu thập someClass cá thể?


4
Đối với người không quen biết, Chrome cho phép bạn kiểm tra các biến / đối tượng nào được thu gom rác và khi nào điều đó xảy ra?
nnnnnn

1
Có lẽ giao diện điều khiển đang giữ một tham chiếu đến nó. Liệu nó có được GCed khi bạn xóa giao diện điều khiển?
David

1
@david Trong ví dụ cuối, unreachablehàm không bao giờ được thực thi nên không có gì thực sự được ghi lại.
James Montagne

1
Tôi gặp khó khăn khi tin rằng một lỗi có tầm quan trọng đó đã xảy ra, ngay cả khi chúng ta dường như phải đối mặt với sự thật. Tuy nhiên tôi đang xem mã nhiều lần và tôi không tìm thấy bất kỳ lời giải thích hợp lý nào khác. Bạn đã cố gắng không chạy mã trong bảng điều khiển cả (hay để trình duyệt chạy mã tự nhiên từ tập lệnh được tải)?
plalx

1
@some, tôi đã đọc bài viết đó trước đây. Nó có phụ đề "Xử lý các tham chiếu vòng tròn trong các ứng dụng JavaScript", nhưng mối quan tâm của các tham chiếu vòng tròn JS / DOM áp dụng cho không có trình duyệt hiện đại. Nó đề cập đến việc đóng cửa, nhưng trong tất cả các ví dụ, các biến trong câu hỏi vẫn được chương trình sử dụng.
Paul Draper

Câu trả lời:


78

Theo như tôi có thể nói, đây không phải là một lỗi mà là hành vi dự kiến.

Từ trang quản lý bộ nhớ của Mozilla : "Kể từ năm 2012, tất cả các trình duyệt hiện đại đều cung cấp trình thu gom rác đánh dấu và quét." "Giới hạn: các đối tượng cần phải được thực hiện rõ ràng không thể truy cập được " .

Trong ví dụ của bạn, nơi nó thất bại somevẫn có thể truy cập được trong bao đóng. Tôi đã thử hai cách để làm cho nó không thể truy cập và cả hai đều hoạt động. Hoặc bạn đặt some=nullkhi bạn không cần nó nữa, hoặc bạn đặt window.f_ = null;và nó sẽ biến mất.

Cập nhật

Tôi đã thử nó trong Chrome 30, FF25, Opera 12 và IE10 trên Windows.

Các tiêu chuẩn không nói bất cứ điều gì về thu gom rác thải, nhưng đưa ra một số manh mối về những gì nên xảy ra.

  • Mục 13 Định nghĩa hàm, bước 4: "Đặt đóng là kết quả của việc tạo đối tượng Hàm mới như được chỉ định trong 13.2"
  • Mục 13.2 "Môi trường từ điển được chỉ định bởi Phạm vi" (scope = clos)
  • Mục 10.2 Môi trường từ điển:

"Tham chiếu bên ngoài của Môi trường từ điển (bên trong) là một tham chiếu đến Môi trường từ điển bao quanh một cách hợp lý môi trường từ điển bên trong.

Tất nhiên, một Môi trường Ngôn ngữ học bên ngoài có thể có Môi trường Ngôn ngữ học bên ngoài riêng. Một môi trường từ điển có thể đóng vai trò là môi trường bên ngoài cho nhiều môi trường từ điển bên trong. Ví dụ: nếu một Tuyên bố chức năng chứa hai Tuyên bố chức năng lồng nhau thì Môi trường từ điển của mỗi chức năng lồng nhau sẽ có như Môi trường từ điển bên ngoài của chúng, Môi trường từ điển của việc thực hiện chức năng xung quanh. "

Vì vậy, một chức năng sẽ có quyền truy cập vào môi trường của cha mẹ.

Vì vậy, somenên có sẵn trong việc đóng hàm trả về.

Vậy thì tại sao nó không luôn có sẵn?

Có vẻ như Chrome và FF đủ thông minh để loại bỏ biến trong một số trường hợp, nhưng trong cả Opera và IE, somebiến này đều có sẵn trong bao đóng (NB: để xem điều này đặt điểm dừng return nullvà kiểm tra trình gỡ lỗi).

GC có thể được cải thiện để phát hiện nếu someđược sử dụng hay không trong các chức năng, nhưng nó sẽ phức tạp.

Một ví dụ tồi:

var someClass = function() {};

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

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

Trong ví dụ ở trên, GC không có cách nào để biết biến đó có được sử dụng hay không (mã được kiểm tra và hoạt động trong Chrome30, FF25, Opera 12 và IE10).

Bộ nhớ được giải phóng nếu tham chiếu đến đối tượng bị phá vỡ bằng cách gán giá trị khác cho window.f_.

Theo tôi đây không phải là một lỗi.


4
Nhưng, một khi cuộc setTimeout()gọi lại chạy, phạm vi chức năng của cuộc setTimeout()gọi lại được thực hiện và toàn bộ phạm vi đó sẽ được thu gom rác, giải phóng tham chiếu của nó some. Không còn bất kỳ mã nào có thể chạy có thể đạt đến thể hiện sometrong bao đóng. Nó nên được thu gom rác. Ví dụ cuối cùng thậm chí còn tồi tệ hơn vì unreachable()thậm chí không được gọi và không ai có liên quan đến nó. Phạm vi của nó cũng nên được phân loại. Cả hai đều có vẻ như lỗi. Không có yêu cầu ngôn ngữ nào trong JS để "giải phóng" mọi thứ trong phạm vi chức năng.
jfriend00

1
@some Không nên. Hàm không được phép đóng trên các biến mà chúng không sử dụng trong nội bộ.
plalx

2
Nó có thể được truy cập bởi hàm rỗng, nhưng không phải vì vậy không có tài liệu tham khảo thực sự nào cho nó nên nó phải rõ ràng. Bộ sưu tập rác theo dõi các tài liệu tham khảo thực tế. Không nên giữ mọi thứ có thể được tham chiếu, chỉ có những thứ thực sự được tham chiếu. Một khi cuối cùng f()được gọi, không có tài liệu tham khảo thực tế somenữa. Nó là không thể truy cập và nên được phân loại.
jfriend00

1
@ jfriend00 Tôi không thể tìm thấy bất cứ điều gì trong (tiêu chuẩn) [ ecma-i Intl.org/publications/files/ECMA-ST/Ecma-262.pdf] nói bất cứ điều gì về chỉ các biến mà nó sử dụng trong nội bộ nên có sẵn. Trong phần 13, bước sản xuất 4: Cho phép đóng là kết quả của việc tạo một đối tượng Hàm mới như được chỉ định trong 13.2 , 10.2 "Tham chiếu môi trường bên ngoài được sử dụng để mô hình lồng nhau logic của các giá trị Môi trường từ điển. Tham chiếu bên ngoài của a (bên trong ) Môi trường từ điển là một tham chiếu đến Môi trường từ điển bao quanh một cách hợp lý môi trường từ điển bên trong. "
một số

2
Vâng, evallà trường hợp thực sự đặc biệt. Ví dụ: evalkhông thể được đặt bí danh ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/ tựa ), vd var eval2 = eval. Nếu evalđược sử dụng (và vì nó không thể được gọi bằng một tên khác, điều đó dễ thực hiện), thì chúng ta phải cho rằng nó có thể sử dụng bất cứ thứ gì trong phạm vi.
Paul Draper

49

Tôi đã thử nghiệm điều này trong IE9 + và 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);

Trang web trực tiếp ở đây .

Tôi hy vọng sẽ kết thúc với một mảng 500 function() {}, sử dụng bộ nhớ tối thiểu.

Thật không may, đó không phải là trường hợp. Mỗi hàm trống giữ một mảng (mãi mãi không thể truy cập được, nhưng không phải là GC) của một triệu số.

Cuối cùng Chrome dừng lại và chết, Firefox hoàn thành toàn bộ sau khi sử dụng gần 4GB RAM và IE phát triển chậm hơn một cách bất thường cho đến khi nó hiển thị "Hết bộ nhớ".

Loại bỏ một trong những dòng nhận xét sẽ sửa chữa mọi thứ.

Dường như cả ba trình duyệt này (Chrome, Firefox và IE) đều giữ một bản ghi môi trường cho mỗi bối cảnh, không phải mỗi lần đóng. Boris đưa ra giả thuyết lý do đằng sau quyết định này là hiệu suất, và điều đó dường như có khả năng, mặc dù tôi không chắc nó có thể được gọi như thế nào trong ánh sáng của thí nghiệm trên.

Nếu cần một tham chiếu đóng cửa some(được cho là tôi đã không sử dụng nó ở đây, nhưng hãy tưởng tượng tôi đã làm), nếu thay vì

function g() { some; }

tôi sử dụng

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

nó sẽ khắc phục các vấn đề về bộ nhớ bằng cách di chuyển bao đóng sang một bối cảnh khác với chức năng khác của tôi.

Điều này sẽ làm cho cuộc sống của tôi tẻ nhạt hơn nhiều.

Vì tò mò, tôi đã thử điều này trong Java (sử dụng khả năng của nó để định nghĩa các lớp bên trong các hàm). GC hoạt động như tôi đã hy vọng ban đầu cho Javascript.


Tôi nghĩ rằng việc đóng dấu ngoặc đơn bị bỏ lỡ cho hàm ngoài var g = (function (some) {return function () {some;};}) (some);
HCJ

15

Heuristic khác nhau, nhưng một cách phổ biến để thực hiện loại điều này là tạo một bản ghi môi trường cho mỗi cuộc gọi đến f()trong trường hợp của bạn và chỉ lưu trữ các địa phương của fnó thực sự bị đóng (bởi một số đóng) trong bản ghi môi trường đó. Sau đó, bất kỳ đóng cửa được tạo ra trong cuộc gọi để fgiữ cho bản ghi môi trường sống. Tôi tin rằng đây là cách Firefox thực hiện việc đóng cửa, ít nhất.

Điều này có lợi ích của việc truy cập nhanh vào các biến đóng và đơn giản thực hiện. Nó có nhược điểm của hiệu ứng quan sát được, trong đó việc đóng cửa tồn tại trong thời gian ngắn đối với một số biến khiến nó được giữ sống nhờ các lần đóng cửa tồn tại lâu.

Người ta có thể thử tạo nhiều bản ghi môi trường cho các lần đóng khác nhau, tùy thuộc vào những gì chúng thực sự đóng, nhưng điều đó có thể trở nên rất phức tạp rất nhanh và có thể gây ra các vấn đề về hiệu năng và bộ nhớ của chính nó ...


Cảm ơn bạn đã hiểu biết của bạn. Tôi đã đi đến kết luận đây cũng là cách Chrome thực hiện đóng cửa. Tôi luôn nghĩ rằng chúng được thực hiện theo cách thứ hai, trong đó mỗi lần đóng chỉ giữ môi trường mà nó cần, nhưng đó không phải là trường hợp. Tôi tự hỏi liệu nó thực sự phức tạp để tạo ra nhiều bản ghi môi trường. Thay vì tổng hợp các tham chiếu của các bao đóng, hãy hành động như thể mỗi một là các bao đóng duy nhất. Tôi đã đoán rằng những cân nhắc về hiệu suất là lý do ở đây, mặc dù với tôi hậu quả của việc có một hồ sơ môi trường được chia sẻ dường như còn tồi tệ hơn.
Paul Draper

Cách thứ hai trong một số trường hợp dẫn đến sự bùng nổ về số lượng hồ sơ môi trường cần được tạo. Trừ khi bạn cố gắng chia sẻ chúng qua các chức năng khi bạn có thể, nhưng sau đó bạn cần một loạt các máy móc phức tạp để làm điều đó. Điều đó là có thể, nhưng tôi đã nói rằng sự đánh đổi hiệu suất ủng hộ cách tiếp cận hiện tại.
Boris Zbarsky

Số lượng hồ sơ bằng với số lần đóng được tạo. Tôi có thể mô tả O(n^2)hoặc O(2^n)như một vụ nổ, nhưng không tăng theo tỷ lệ.
Paul Draper

Chà, O (N) là một vụ nổ so với O (1), đặc biệt là khi mỗi người có thể chiếm một lượng bộ nhớ khá lớn ... Một lần nữa, tôi không phải là chuyên gia về vấn đề này; hỏi trên kênh #jsapi trên irc.mozilla.org có thể giúp bạn giải thích rõ hơn và chi tiết hơn những gì tôi có thể cung cấp về sự đánh đổi là gì.
Boris Zbarsky

1
@Esailija Thật sự khá phổ biến. Tất cả những gì bạn cần là một hàm tạm thời lớn (thường là một mảng được gõ lớn) mà một số sử dụng gọi lại ngẫu nhiên trong thời gian ngắn và đóng cửa tồn tại lâu. Gần đây đã xuất hiện một số lần cho những người viết ứng dụng web ...
Boris Zbarsky

0
  1. Duy trì trạng thái giữa các lệnh gọi Giả sử bạn có hàm add () và bạn muốn nó thêm tất cả các giá trị được truyền vào nó trong một số cuộc gọi và trả về tổng.

thích thêm (5); // trả về 5

thêm (20); // trả về 25 (5 + 20)

thêm (3); // trả về 28 (25 + 3)

Hai cách bạn có thể làm điều này trước tiên là bình thường xác định một biến toàn cục Tất nhiên, bạn có thể sử dụng một biến toàn cục để giữ tổng số. Nhưng hãy nhớ rằng anh chàng này sẽ ăn sống bạn nếu bạn (ab) sử dụng toàn cầu.

bây giờ cách mới nhất bằng cách sử dụng đóng với định nghĩa biến toàn cục

(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


vui lòng mô tả câu trả lời
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
    
  
}());

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.