Как блокировка JavaScript - сбор мусора

Я зарегистрировал следующую ошибку 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();

И соответствующий снимок экрана:

screenshot of Chromebug

Кажется, что замыкание (в данном случае function() {}) сохраняет все объекты "живыми", если объект ссылается на любое другое закрытие в том же контексте, независимо от того, является ли это закрытие даже достижимым.

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

В каком из этих трех случаев мусор IE9 + и Firefox собирает экземпляр someClass ?

Ответ 1

Я тестировал это в 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);

он устранит проблемы с памятью, переместив замыкание в другой контекст, чем моя другая функция.

Это сделает мою жизнь намного более утомительной.

P.S. Из любопытства я попробовал это на Java (используя его способность определять классы внутри функций). GC работает так, как я изначально надеялся на Javascript.

Ответ 2

Насколько я могу судить, это не ошибка, а ожидаемое поведение.

Из Mozilla Страница управления памятью: "Начиная с 2012 года, все современные браузеры поставляют сборщик мусора и разметки". "Ограничение: объекты должны быть явно недоступны" .

В ваших примерах, где он терпит неудачу some по-прежнему доступен в закрытии. Я попробовал два способа сделать его недоступным и работать. Либо вы установите some=null, когда вам это больше не понадобится, либо установите window.f_ = null;, и он исчезнет.

Обновление

Я пробовал это в Chrome 30, FF25, Opera 12 и IE10 в Windows.

стандарт ничего не говорит о сборке мусора, но дает некоторые подсказки о том, что должно произойти.

  • Раздел 13 Определение функции, шаг 4: "Пусть замыкание является результатом создания нового объекта Function, указанного в 13.2"
  • Раздел 13.2 "Лексическая среда, заданная областью действия" (область действия = закрытие)
  • Раздел 10.2 Лексическая среда:

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

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

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

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

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

Кажется, что Chrome и FF достаточно умен, чтобы в какой-то мере исключить эту переменную, но в Opera и IE переменная some доступна в закрытии (NB: для просмотра этого набора точка останова на 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_.

По-моему, это не ошибка.

Ответ 3

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

Это имеет преимущества быстрого доступа к закрытым переменным и простоте реализации. У этого есть недостаток наблюдаемого эффекта, когда короткоживущее закрытие, закрывающее какую-то переменную, заставляет его сохранять живые долгоживущие затворы.

Можно попытаться создать несколько записей среды для разных закрытий, в зависимости от того, что они фактически закрывают, но это может очень быстро усложниться и может вызвать проблемы с производительностью и памятью...