Мастер-классы по Javascript Екатеринбург Ростов-на-Дону Москва Узнать больше...
Содержание (скрыть) Содержание (показать)

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

  1. Коллекция утечек в IE
    1. Утечка DOM ↔ JS в IE<8
    2. Утечка DOM ↔ JS, вариант для IE<9
    3. Утечка IE8 при обращении к свойствам таблицы
    4. Утечка через XmlHttpRequest в IE<9
  2. Объемы утечек памяти
  3. jQuery и борьба с утечками
    1. Примеры утечек в jQuery
    2. Используем jQuery без утечек
  4. Поиск и устранение утечек памяти
    1. Проверка на утечки
    2. Настройка браузера
  5. Инструменты

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

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

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

Это случается из-за ошибок браузера, проблем с расширениями браузера и, гораздо реже, по причине ошибок в архитектуре вашего кода.

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

Утечка DOM ↔ JS в IE<8

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

Проблема была особенно серьезна в IE6 до SP3 (или патча середины 2007 года), в котором память не освобождалась даже после выгрузки страницы.

Функция setHeandler в примере ниже приведёт к утечке памяти в IE6,7:

function setHandler() {
  var elem = document.getElementById('id');
  elem.onclick = function() {  
    /* ... */ 
  };
}

Элемент elem ссылается на JavaScript-функцию через ссылку onclick, а она — ссылается на elem через замыкание.

Здесь вместо DOM-элемента в IE может быть XMLHttpRequest, ActiveX, любой другой COM-объект. Круговая ссылка гарантирует утечку.

Обойти утечки памяти в IE можно, разорвав циклические ссылки.

Например, можно удалить ссылку на elem из замыкания, присвоив elem = null. Таким образом обработчик больше не ссылается на DOM-элемент. Циклическая ссылка разорвана:

Больше информации об этой утечке вы можете почерпнуть из статей: Understanding and Solving Internet Explorer Leak Patterns и Circular Memory Leak Mitigation.

Утечка DOM ↔ JS, вариант для IE<9

В браузере 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);
}

Открыть в новом окне (в IE)

Этот пример, течёт в IE6,7,8, а также в IE9 в режиме совместимости с IE8.

Проблема — в круговой ссылке elem.__expando__elem = elem. Она может возникать и неявным образом, через замыкание:

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

Открыть в новом окне (в IE)

Без метода method здесь утечки не будет.

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

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

Код:

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 очищен, всё честно
}

Открыть в новом окне (в IE)

  • Если убрать отмеченную строку, то утечки не будет.
  • Если заменить сроку (*) на elem.innerHTML = '', то память будет очищена, т.к. этот способ работает по-другому, нежели просто removeChild (см. главу Управление памятью в браузере).

Ряд JavaScript-фреймворков использует для очистки элемента такую функцию:

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

При регулярном обновлении больших таблиц (гридов и т.п.) и использовании табличных псевдо-массивов — в IE8 будет утечка.

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

Утечка через XmlHttpRequest в IE<9

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

function leak() {
  var xhr = new XMLHttpRequest(); // в IE6 создать через ActiveX

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

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

  xhr.send(null);
}

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

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

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

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

Открыть в новом окне (в IE<9) (откройте страницу и пусть поработает минут 20 - съест всю память, включая виртуальную).

Чтобы это исправить, нам нужно разорвать круговую ссылку 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);
}

Теперь циклической ссылки нет — мы устранили утечку.

Посмотреть исправленный пример для IE в отдельном окне.

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

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

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

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

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

  function inner() {
    // ...
  }

  return inner;
}

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

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

Интерпертатор JavaScript не знает, какие из переменных функции inner будут использованы, поэтому оставляет их все.

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

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

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

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

  function inner() {
    // ...
  }

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

  return inner;
}

Сейчас data все еще остается в памяти как свойство LexicalEnvironment, но он уже равен null, а строка удалена из памяти.

jQuery и борьба с утечками

В jQuery для борьбы с утечками памяти в IE6-7 используется $.data API. Однако, это может стать причиной новых утечек, характерных для jQuery.

Основной принцип $.data — это для любого JavaScript объекта сохранить/получить значение в/из элемента с помощью jQuery вызова:

// работает, т.к. исползует jQuery

$(document.body).data('prop', 'val') // set
alert( $(document.body).data('prop') ) // get

jQuery $(elem).data(prop, val) делает следующее:

  1. Элемент получает уникальный идентификатор, если у него такого еще нет:
    elem[ jQuery.expando ] = id = ++jQuery.uuid;  // средствами jQuery
    
    jQuery.expando - это случайное значение-иденификатор, исключающее конфликты.
  2. Даные сохраняются в специальном объекте jQuery.cache:
    jQuery.cache[id]['prop'] = val;
    

Когда данные считываются из элемента:

  1. Уникальный идентификатор лемента извлекается из id = elem[ jQuery.expando].
  2. Данные считываются из jQuery.cache[id].

Смысл этого API в том, что DOM-элемент никогда не ссылается на JavaScript объект напрямую. Задействуется идентификатор. Плюс в том, что это безопасно. Данные хранятся в jQuery.cache. К тому же внутренние обработчики событий используют $.data API.

Но как побочный эффект - элемент не может быть удален из DOM используя собственные вызовы.

Примеры утечек в jQuery

Следующий код создает утечку во всех браузерах:

$('<div/>')
  .html(new Array(1000).join('text')) // div с текстом, возможна AJAX-загрузка
  .click(function() { })
  .appendTo('#data')

document.getElementById('data').innerHTML = '';

Показать в отдельном окне

Утечка происходит потому, что elem#data удален очисткой родительского innerHTML, но в jQuery.cache данные остались. Более того, обработчик события ссылается на elem#data, так что оба - и обработчик и elem - остаются в памяти вместе со всем замыканием.

Простой пример утечки:

Этот код создает утечку:

function go() {
  $('<div/>')
    .html(new Array(1000).join('text')) 
    .click(function() { })
}

Показать в отдельном окне

Проблема вот в чем: элемент создан но нигде не размещен. После выполнения функции ссылка на него теряется. Но данные сохраняются в jQuery.cache.

Используем jQuery без утечек

Чтобы избежать утечек, описанных выше, для удаления элементов используйте функции jQuery API, а не чистый JavaScript.

Методы remove(), empty() и html() проверяют дочерние элемены на наличие данных и очищают их. Это несколько замедляет процедуру удаления, но зато освобождается память.

Обычно, если производительность очень важна, используют дополнительные трюки.

  1. Если вы знаете что элементы имеют обработчики (и их немного) вы можете вручную очистить данные с помощью removeData() и обезопаситься от утечки.
    Теперь можно воспользоваться методом detach(), который не очищает данные и собственные методы
  2. Если метод, описаный выше, вам не нравиться, так как DOM дерево большое, то можете использовать $elem.detach() и поместить $(elem).remove() в setTimeout. В результате очистка будет происходить ассинхронно и незаметно.

К счастью обнаружить утечки памяти jQuery легко. Проверьте размер $.cache. Если размер большой, то изучите кэш, посмотите, какие записи не меняются, остаются и почему.

Итак, будем надеятся, что эта тема для вас теперь прозрачна и ясна и в следующих разделах мы не будем больше говорить об утечках в jQuery.

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

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

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

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

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

Если речь об IE, то надо смотреть «Виртуальную память» в списке процессов, а не только обычную «Память». Обычная может очищаться за счет того, что перемещается в виртуальную (на диск).

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

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

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

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

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


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

Инструменты

В Chrome используйте Инструменты разработчика - Timeline - Memory tab - таблицу + график использования памяти.

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

Также Profiles - Memory, здесь можно сделать и исследовать снимок текущего состояния страницы. Снимки можно сравнивать друг с другом.

Замечательная статья: Chrome Developer Tools: Heap Profiling.

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

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

Перевести с английского варианта
учебника помог Марат Шагиев

См. также:

Комментарии

  1. Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  2. Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
  3. Комментарии без смысла, с рекламой или не о статье вообще - удаляются.
Наверх

Содержание

Реклама

Нашли опечатку?

Нашли опечатку на сайте? Что-то кажется странным?
Выделите соответствующий текст и нажмите Ctrl+Enter!

Последние Комментарии

Помоги другим!

Помоги другим узнать о хорошей статье!