Яким чином закриття JavaScript збирає сміття


168

Я зареєстрував таку помилку Chrome , що призвело до багатьох серйозних і неочевидних витоків пам'яті в моєму коді:

(У цих результатах використовується профайлер пам'яті Chrome Dev Tools , який запускає GC, а потім робить знімок всього, що не збирається зібрано.)

У наведеному нижче коді someClassекземпляр збирається сміттям (добре):

var someClass = function() {};

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

window.f_ = f();

Але в цьому випадку сміття не буде зібрано (погано):

var someClass = function() {};

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

window.f_ = f();

І відповідний знімок екрана:

скріншот Chromebug

Схоже, що закриття (в даному випадку function() {}) зберігає всі об'єкти "живими", якщо на об'єкт посилається будь-яке інше закриття в тому ж контексті, незалежно від того, чи є це закриття навіть доступним.

Моє запитання - про збирання сміття в інших браузерах (IE 9+ та Firefox). Я добре знайомий з інструментами webkit, такими як профільний купон JavaScript, але мало знаю інших інструментів браузера, тому мені не вдалося перевірити це.

У якому з цих трьох випадків сміття IE9 + та Firefox збиратиме someClass екземпляр?


4
Для незнайомих, як Chrome дозволяє вам перевірити, які змінні / об’єкти збирають сміття, і коли це відбувається?
nnnnnn

1
Можливо, консоль зберігає посилання на неї. Він отримує GCed, коли ви очищаєте консоль?
Девід

1
@david В останньому прикладі unreachableфункція ніколи не виконується, тому насправді нічого не реєструється.
Джеймс Монтань

1
Мені важко вірити, що помилка такої важливості пережила, навіть якщо ми, здається, стикаємося з фактами. Однак я переглядаю код знову і знову, і не знаходжу іншого раціонального пояснення. Ви намагалися взагалі не запускати код у консолі (він же дозволив браузеру запускати його природним чином із завантаженого сценарію)?
plalx

1
@ хтось, я читав цю статтю раніше. Він має підзаголовок "Обробка кругових посилань у додатках JavaScript", але турбота про циркулярні посилання JS / DOM стосується жодного сучасного браузера. У ньому згадуються про закриття, але у всіх прикладах ці змінні змінні все ще використовувались програмою.
Пол Дрейпер

Відповіді:


78

Наскільки я можу сказати, це не помилка, а очікувана поведінка.

На сторінці управління пам’яттю Mozilla : «З 2012 року всі сучасні браузери доставляють сміттєзбірник». "Обмеження: об'єкти потрібно робити явно недоступними " .

У ваших прикладах, де вона не вдається some, все ще можна дістатись до закриття. Я спробував два способи зробити це недосяжним, і обидва працюють. Або ви встановите, some=nullколи він вам більше не потрібен, або ви встановите, window.f_ = null;і він пропаде.

Оновлення

Я спробував це в Chrome 30, FF25, Opera 12 та IE10 в Windows.

Стандарт нічого не знаю про збірку сміття не говорить, але дає деякі підказки про те, що має статися.

  • Розділ 13 Визначення функції, крок 4: "Нехай закриття є результатом створення нового об'єкта Функції, як зазначено в 13.2"
  • Розділ 13.2 "Лексичне середовище, визначене сферою дії" (сфера дії = закриття)
  • Розділ 10.2 Лексичні середовища:

"Зовнішня посилання на (внутрішнє) лексичне середовище - це посилання на лексичне середовище, що логічно оточує внутрішнє лексичне середовище.

Зовнішнє лексичне середовище, звичайно, може мати своє зовнішнє лексичне середовище. Лексичне середовище може служити зовнішнім середовищем для кількох внутрішніх лексичних середовищ. Наприклад, якщо декларація функції містить дві вкладені декларації функції, то лексичне середовище кожної з вкладених функцій матиме як їх зовнішнє лексичне середовище лексичне середовище поточного виконання оточуючої функції. "

Отже, функція матиме доступ до оточення батьків.

Отже, someповинні бути доступні при закритті функції, що повертається.

Тоді чому він не завжди доступний?

Здається, що Chrome і FF досить розумні, щоб усунути змінну в деяких випадках, але і в Opera, і в IE someзмінна доступна у закритті (Примітка: для перегляду цього встановлення точки перерви return nullта перевірки налагоджувача).

ГК можна вдосконалити, щоб виявити some, використовується він чи ні у функціях, але це буде складним.

Поганий приклад:

var someClass = function() {};

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

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

У прикладі вище GC не може знати, застосовується чи ні змінна (тестується код і працює в Chrome30, FF25, Opera 12 та IE10).

Пам'ять вивільняється, якщо посилання на об'єкт порушено шляхом призначення іншого значення window.f_.

На мою думку, це не помилка.


4
Але після setTimeout()запуску зворотного дзвінка ця область функцій setTimeout()зворотного дзвінка виконана, і вся ця область повинна бути зібрана сміттям, звільняючи від неї посилання some. Більше не існує коду, який може запускатися, який може дійти до екземпляра someу закритті. Це має бути зібране сміття. Останній приклад ще гірший, тому що unreachable()його навіть не називають і ніхто не має посилання на нього. Його сфера також повинна бути GCed. Ці обидва здаються помилками. У JS не існує вимог до мови "звільняти" речі в області функцій.
jfriend00

1
@some Це не повинно. Функції не повинні закриватись змінними, які вони не використовують внутрішньо.
plalx

2
До нього можна отримати доступ із порожньою функцією, але це не так, що на неї немає фактичних посилань, тому це повинно бути зрозуміло. Збір сміття відстежує фактичні довідки. Не слід триматися за все, на що можна було б посилатися, лише на речі, на які є посилання на насправді. Після виклику останнього f()вже немає фактичних посилань some. Це недосяжно і має бути GCed.
jfriend00

1
@ jfriend00 Я не можу знайти нічого (у стандартному) [ ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf], що говорить про лише ті змінні, які він використовує внутрішньо, має бути доступним. У розділі 13 крок виробництва 4: Нехай закриття є результатом створення нового об'єкта Функції, визначеного в 13.2 , 10.2 "Посилання на зовнішнє середовище використовується для моделювання логічного введення значень Лексичного середовища. Зовнішнє посилання на (внутрішнє ) Лексичне середовище - це посилання на лексичне середовище, яке логічно оточує внутрішнє лексичне середовище ".
десь

2
Ну, evalце справді особливий випадок. Наприклад, evalне може бути псевдонімом ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… ), наприклад var eval2 = eval. Якщо evalвін використовується (а оскільки його не можна назвати іншим іменем, це легко зробити), тоді ми повинні припустити, що він може використовувати будь-що в області застосування.
Пол Дрейпер

49

Я перевірив це в IE9 + і 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);

Живий сайт тут .

Я сподівався закінчитись масивом 500 function() {}-х, використовуючи мінімальну пам'ять.

На жаль, це було не так. Кожна порожня функція містить у собі (назавжди недосяжний, але не GC'ed) масив з мільйона чисел.

З часом Chrome зупиняється і відмирає, Firefox закінчує все після використання майже 4 Гб оперативної пам’яті, а IE росте асимптотично повільніше, поки не з’явиться «Поза пам’яттю».

Видалення будь-якого з коментованих рядків виправляє все.

Схоже, що всі три цих браузери (Chrome, Firefox та IE) зберігають записи про навколишнє середовище за контекстом, а не за закриттям. Борис гіпотезує, що причина цього рішення - це ефективність, і це здається вірогідним, хоча я не впевнений, наскільки виконавцем його можна назвати у світлі вищезазначеного експерименту.

Якщо вам потрібне посилання про закриття some(дано, я тут його не використовував, але уявіть, що я зробив), якщо замість цього

function g() { some; }

я використовую

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

це виправить проблеми з пам'яттю, перемістивши закриття в інший контекст, ніж моя інша функція.

Це зробить моє життя набагато втомливішим.

PS З цікавості, я спробував це на Java (використовуючи його здатність визначати класи всередині функцій). GC працює так, як я спочатку сподівався на Javascript.


Я думаю, що дужки закриття пропущені для зовнішньої функції var g = (function (some) {return function () {some;};}) (some);
HCJ

15

Евристика відрізняється, але загальним способом реалізації подібних речей є створення запису про оточення для кожного дзвінка f()у вашому випадку, і зберігати лише місцеві жителі f, які фактично закриті ( деяким закриттям) у цьому записі оточення. Тоді будь-яке закриття, створене під час виклику, щоб fзберегти живий запис середовища. Я вважаю, що саме так Firefox реалізує закриття, принаймні.

Це має переваги швидкого доступу до змінних, що закриваються, та простоти реалізації. Він має недолік спостережуваного ефекту, коли короткочасне закриття над деякою змінною призводить до того, що він зберігається в живих завдяки довготривалим закриттям.

Можна спробувати створити кілька записів навколишнього середовища для різних закриттів, залежно від того, чим вони насправді закриваються, але це може дуже швидко ускладнитися і може спричинити власні проблеми та пам'ять ...


дякую за розуміння Я прийшов до висновку, що Chrome також реалізує закриття. Я завжди думав, що вони реалізовані останнім чином, коли кожне закриття зберігало лише те середовище, яке воно потребувало, але такого не відбувається. Цікаво, чи насправді так складно створювати кілька записів навколишнього середовища. Замість того, щоб узагальнювати посилання про закриття, дійте так, ніби кожне було єдиним закриттям. Я здогадувався, що міркування щодо ефективності тут є міркуваннями, хоча мені наслідки наявності спільного запису про довкілля здаються ще гіршими.
Пол Дрейпер

Останній спосіб у деяких випадках призводить до вибуху в кількості записів навколишнього середовища, які потрібно створити. Якщо тільки ви не намагаєтеся розділити їх на різні функції, коли зможете, але тоді для цього вам потрібна купа складної техніки. Це можливо, але мені кажуть, що компроміси на користь ефективності сприяють поточному підходу.
Борис Збарський

Кількість записів дорівнює кількості створених закриттів. Я міг би описаний O(n^2)або O(2^n)як вибух, але не пропорційне збільшення.
Пол Дрейпер

Що ж, O (N) - це вибух порівняно з O (1), особливо коли кожен може зайняти неабияку кількість пам'яті ... Знову ж таки, я не є експертом у цьому; запитання на каналі #jsapi на irc.mozilla.org, швидше за все, отримає вам краще та більш детальне пояснення, ніж я можу надати, що таке компроміси.
Борис Збарський

1
@Esailija Це насправді досить поширене, на жаль. Все, що вам потрібно, - це великий тимчасовий у функції (як правило, великий набраний масив), який використовують випадкові короткочасні зворотні дзвінки та довготривале закриття. Останнім часом це неодноразово з'являється для людей, які пишуть веб-програми ...
Борис Збарський

0
  1. Підтримуйте стан між викликами функцій Скажімо, у вас є функція add (), і ви хочете, щоб вона додала всі значення, передані їй у декілька викликів, і повертала суму.

як додати (5); // повертає 5

додати (20); // повертає 25 (5 + 20)

додати (3); // повертає 28 (25 + 3)

два способи зробити це спочатку - це нормально визначити глобальну змінну Звичайно, ви можете використовувати глобальну змінну, щоб утримувати загальну величину. Але майте на увазі, що цей чувак з'їсть вас живими, якщо ви (аб) будете користуватися глобальними.

Тепер останній спосіб використання закриття з out визначити глобальну змінну

(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


опишіть будь-ласка відповідь
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
    
  
}());

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.