Как закрытия 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: «Пусть замыкание будет результатом создания нового объекта Function, как указано в 13.2»
  • Раздел 13.2 «Лексическая среда, определенная Scope» (scope = closure)
  • Раздел 10.2 Лексические среды:

«Внешняя ссылка на (внутреннюю) Лексическую Среду - это ссылка на Лексическую Среду, которая логически окружает внутреннюю Лексическую Среду.

Внешняя Лексическая Среда может, конечно, иметь свою собственную внешнюю Лексическую Среду. Лексическая среда может служить внешней средой для нескольких внутренних Лексических сред. Например, если объявление функции содержит два вложенных объявления функции, то в качестве внешней лексической среды для каждой из вложенных функций в качестве внешней лексической среды будет использоваться лексическая среда текущего выполнения окружающей функции ».

Таким образом, функция будет иметь доступ к среде родителя.

Итак, someдолжно быть доступно при закрытии возвращающей функции.

Тогда почему это не всегда доступно?

Кажется, что Chrome и FF достаточно умны, чтобы исключить переменную в некоторых случаях, но и в Opera, и в IE someпеременная доступна в замыкании (примечание: для просмотра этого установите точку останова return nullи проверьте отладчик).

GC можно улучшить, чтобы определить, 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
@ Некоторые не должны. Функции не должны закрывать переменные, которые они не используют внутри.
plalx
2
К ней может обращаться пустая функция, но это не так, поэтому на нее нет реальных ссылок, поэтому она должна быть понятной. Сборка мусора отслеживает фактические ссылки. Он не должен содержать все, на что можно было сослаться, только то, на что действительно ссылаются. Как только последний f()вызван, больше нет никаких реальных ссылок на some. Это недоступно и должно быть GCed.
jfriend00
1
@ jfriend00 Я не могу найти ничего в (стандартном) [ ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf] говорит что-то только о переменных, которые он использует внутри, должны быть доступны. В разделе 13 производственный этап 4: Пусть замыкание будет результатом создания нового объекта Function, как указано в 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) массив из миллиона чисел.

В конечном итоге 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)

это можно сделать двумя способами: во-первых, обычно определяют глобальную переменную. Конечно, вы можете использовать глобальную переменную для хранения итогового значения. Но имейте в виду, что этот чувак съест вас заживо, если вы (ab) будете использовать глобалы.

теперь последний способ использования замыкания без определения глобальной переменной

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

Авинаш Маурья
источник