Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
302 lines (195 sloc) 16.9 KB

Утечки памяти

Утечки памяти происходят, когда браузер по какой-то причине не может освободить память от недостижимых объектов.

Обычно это происходит автоматически (info:memory-management). Кроме того, браузер освобождает память при переходе на другую страницу. Поэтому утечки в реальной жизни проявляют себя в двух ситуациях:

  1. Приложение, в котором посетитель все время на одной странице и работает со сложным JavaScript-интерфейсом. В этом случае утечки могут постепенно съедать доступную память.
  2. Страница регулярно делает что-то, вызывающее утечку памяти. Посетитель (например, менеджер) оставляет компьютер на ночь включенным, чтобы не закрывать браузер с кучей вкладок. Приходит утром -- а браузер съел всю память и рухнул и сильно тормозит.

Утечки бывают из-за ошибок браузера, ошибок в расширениях браузера и, гораздо реже, по причине ошибок в архитектуре JavaScript-кода. Мы разберём несколько наиболее частых и важных примеров.

Коллекция утечек в IE

Утечка DOM ↔ JS в IE8-

IE до версии 8 не умел очищать циклические ссылки, появляющиеся между DOM-объектами и объектами JavaScript. В результате и DOM и JS оставались в памяти навсегда.

В браузере IE8 была проведена серьёзная работа над ошибками, но утечка в IE8- появляется, если круговая ссылка возникает "через объект".

Чтобы было понятнее, о чём речь, посмотрите на следующий код. Он вызывает утечку памяти в IE8-:

function leak() {
  // Создаём новый DIV, добавляем к BODY
  var elem = document.createElement('div');
  document.body.appendChild(elem);

  // Записываем в свойство жирный объект
  elem.__expando = {
    bigAss: new Array(1000000).join('lalala')
  };

*!*
  // Создаём круговую ссылку. Без этой строки утечки не будет.
  elem.__expando.__elem = elem;
*/!*

  // Удалить элемент из DOM. Браузер должен очистить память.
  elem.parentElement.removeChild(elem);
}

Полный пример (только для IE8-, а также IE9 в режиме совместимости с IE8):

[codetabs src="leak-ie8"]

Круговая ссылка и, как следствие, утечка может возникать и неявным образом, через замыкание:

function leak() {
  var elem = document.createElement('div');
  document.body.appendChild(elem);

  elem.__expando = {
    bigAss: new Array(1000000).join('lalala'),
*!*
    method: function() {} // создаётся круговая ссылка через замыкание
*/!*
  };

  // Удалить элемент из DOM. Браузер должен очистить память.
  elem.parentElement.removeChild(elem);
}

Полный пример (IE8-, IE9 в режиме совместимости с IE8):

[codetabs src="leak-ie8-2"]

Без привязки метода method к элементу здесь утечки не возникнет.

Бывает ли такая ситуация в реальной жизни? Или это -- целиком синтетический пример, для заумных программистов?

Да, конечно бывает. Например, при разработке графических компонент -- бывает удобно присвоить DOM-элементу ссылку на JavaScript-объект, который представляет собой компонент. Это упрощает делегирование и, в общем-то, логично, что DOM-элемент знает о компоненте на себе. Но в IE8- прямая привязка ведёт к утечке памяти!

Примерно так:

function Menu(elem) {
  elem.onclick = function() {};
}

var menu = new Menu(elem); // Menu содержит ссылку на elem
*!*
elem.menu = menu; // такая привязка или что-то подобное ведёт к утечке в IE8
*/!*

Полный пример (IE8-, IE9 в режиме совместимости с IE8):

[codetabs src="leak-ie8-widget"]

Утечка IE8 при обращении к коллекциям таблицы

Эта утечка происходит только в IE8 в стандартном режиме. В нём при обращении к табличным псевдо-массивам (напр. rows) создаются и не очищаются внутренние ссылки, что приводит к утечкам.

Также воспроизводится в новых IE в режиме совместимости с IE8.

Код:

var elem = document.createElement('div'); // любой элемент

function leak() {

  elem.innerHTML = '<table><tr><td>1</td></tr></table>';

*!*
  elem.firstChild.rows[0]; // просто доступ через rows[] приводит к утечке
  // при том, что мы даже не сохраняем значение в переменную
*/!*

  elem.removeChild(elem.firstChild); // удалить таблицу (*)
  // alert(elem.childNodes.length) // выдал бы 0, elem очищен, всё честно
}

Полный пример (IE8):

[codetabs src="leak-ie8-table"]

Особенности:

  • Если убрать отмеченную строку, то утечки не будет.
  • Если заменить строку (*) на elem.innerHTML = '', то память будет очищена, т.к. этот способ работает по-другому, нежели просто removeChild (см. главу info:memory-management).
  • Утечка произойдёт не только при доступе к rows, но и к другим свойствам, например elem.firstChild.tBodies[0].

Эта утечка проявляется, в частности, при удалении детей элемента следующей функцией:

function empty(elem) {
  while (elem.firstChild) elem.removeChild(elem.firstChild);
}

Если идёт доступ к табличным коллекциям и регулярное обновление таблиц при помощи DOM-методов -- утечка в IE8 будет расти.

Более подробно вы можете почитать об этой утечке в статье Утечки памяти в IE8, или страшная сказка со счастливым концом.

Утечка через XmlHttpRequest в IE8-

Следующий код вызывает утечки памяти в IE8-:

function leak() {
  var xhr = new XMLHttpRequest();

  xhr.open('GET', '/server.do', true);

  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
      // ...
    }
  }

  xhr.send(null);
}

Как вы думаете, почему? Если вы внимательно читали то, что написано выше, то имеете информацию для ответа на этот вопрос..

Посмотрим, какая структура памяти создается при каждом запуске:

Когда запускается асинхронный запрос xhr, браузер создаёт специальную внутреннюю ссылку (internal reference) на этот объект и будет поддерживать её, пока он находится в процессе коммуникации. Именно поэтому объект xhr будет жив после окончания работы функции.

Когда запрос завершен, браузер удаляет внутреннюю ссылку, xhr становится недостижимым и память очищается... Везде, кроме IE8-.

Полный пример (IE8):

[codetabs src="leak-ie8-xhr"]

Чтобы это исправить, нам нужно разорвать круговую ссылку XMLHttpRequest ↔ JS. Например, можно удалить xhr из замыкания:

function leak() {
  var xhr = new XMLHttpRequest();

  xhr.open('GET', 'something.js?' + Math.random(), true);

  xhr.onreadystatechange = function() {
    if (xhr.readyState != 4) return;

    if (xhr.status == 200) {
      document.getElementById('test').innerHTML++;
    }

*!*
    xhr = null; // по завершении запроса удаляем ссылку из замыкания
*/!*
  }

  xhr.send(null);
}

Теперь циклической ссылки нет -- и не будет утечки.

Объемы утечек памяти

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

Представьте, вы создали функцию, и одна из ее переменных содержит очень большую по объему строку (например, получает с сервера).

function f() {
  var data = "Большой объем данных, например, переданных сервером"

  /* делаем что-то хорошее (ну или плохое) с полученными данными */

  function inner() {
    // ...
  }

  return inner;
}

Пока функция inner остается в памяти, LexicalEnvironment с переменной большого объема внутри висит в памяти.

Висит до тех пор, пока функция inner жива.

Как правило, JavaScript не знает, какие из переменных функции inner будут использованы, поэтому оставляет их все. Исключение -- виртуальная машина V8 (Chrome, Opera, Node.JS), она часто (не всегда) видит, что переменная не используется во внутренних функциях, и очистит память.

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

Сэкономить память здесь вполне можно. Мы же знаем, что переменная data не используется в inner. Поэтому просто обнулим её:

function f() {
  var data = "Большое количество данных, например, переданных сервером"

  /* действия с data */

  function inner() {
    // ...
  }

*!*
  data = null; // когда data станет не нужна -
*/!*

  return inner;
}

Поиск и устранение утечек памяти

Проверка на утечки

Существует множество шаблонов утечек и ошибок в браузерах, которые могут приводить к утечкам. Для их устранения сперва надо постараться изолировать и воспроизвести утечку.

  • Необходимо помнить, что браузер может очистить память не сразу когда объект стал недостижим, а чуть позже. Например, сборщик мусора может ждать, пока не будет достигнут определенный лимит использования памяти, или запускаться время от времени.

Поэтому если вы думаете, что нашли проблему и тестовый код, запущенный в цикле, течёт -- подождите примерно минуту, добейтесь, чтобы памяти ело стабильно и много. Тогда будет понятно, что это не особенность сборщика мусора.

  • Если речь об IE, то надо смотреть "Виртуальную память" в списке процессов, а не только обычную "Память". Обычная может очищаться за счет того, что перемещается в виртуальную (на диск).
  • Для простоты отладки, если есть подозрение на утечку конкретных объектов, в них добавляют большие свойства-маркеры. Например, подойдет фрагмент текста: new Array(999999).join('leak').

Настройка браузера

Утечки могут возникать из-за расширений браузера, взаимодействющих со страницей. Еще более важно, что утечки могут быть следствием конфликта двух браузерных расширений Например, было такое: память текла когда включены расширения Skype и плагин антивируса одновременно.

Чтобы понять, в расширениях дело или нет, нужно отключить их:

  1. Отключить Flash.
  2. Отключить антивирусную защиту, проверку ссылок и другие модули, и дополнения.
  3. Отключить плагины. Отключить ВСЕ плагины.
  • Для IE есть параметр коммандной строки:
"C:\Program Files\Internet Explorer\iexplore.exe" -extoff
Кроме того необходимо отключить сторонние расширения в свойствах IE.

![](ie9_disable1.png)

  • Firefox необходимо запускать с чистым профилем. Используйте следующую команду для запуска менеджера профилей и создания чистого пустого профиля:
firefox --profilemanager

Инструменты

Пожалуй, единственный браузер с поддержкой отладки памяти -- это Chrome. В инструментах разработчика вкладка Timeline -- Memory показывает график использования памяти.

Можем посмотреть, сколько памяти используется и на что.

Также в Profiles есть кнопка Take Heap Snapshot, здесь можно сделать и исследовать снимок текущего состояния страницы. Снимки можно сравнивать друг с другом, выяснять количество новых объектов. Можно смотреть, почему объект не очищен и кто на него ссылается.

Замечательная статья на эту тему есть в документации: Chrome Developer Tools: Heap Profiling.

Утечки памяти штука довольно сложная. В борьбе с ними вам определенно понадобится одна вещь: Удача!