# Утечки памяти *Утечки памяти* происходят, когда браузер по какой-то причине не может освободить память от недостижимых объектов. Обычно это происходит автоматически ([](/memory-management)). Кроме того, браузер освобождает память при переходе на другую страницу. Поэтому утечки в реальной жизни проявляют себя в двух ситуациях: [cut]
  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"] Круговая ссылка и, как следствие, утечка может возникать и неявным образом, через замыкание: ```js 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- прямая привязка ведёт к утечке памяти! Примерно так: ```js 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. Код: ```js var elem = document.createElement('div'); // любой элемент function leak() { elem.innerHTML = '
1
'; *!* elem.firstChild.rows[0]; // просто доступ через rows[] приводит к утечке // при том, что мы даже не сохраняем значение в переменную */!* elem.removeChild(elem.firstChild); // удалить таблицу (*) // alert(elem.childNodes.length) // выдал бы 0, elem очищен, всё честно } ``` Полный пример (IE8): [codetabs src="leak-ie8-table"] Особенности: Эта утечка проявляется, в частности, при удалении детей элемента следующей функцией: ```js function empty(elem) { while (elem.firstChild) elem.removeChild(elem.firstChild); } ``` Если идёт доступ к табличным коллекциям и регулярное обновление таблиц при помощи DOM-методов -- утечка в IE8 будет расти. Более подробно вы можете почитать об этой утечке в статье [Утечки памяти в IE8, или страшная сказка со счастливым концом](http://habrahabr.ru/post/141451/). ### Утечка через XmlHttpRequest в IE8- Следующий код вызывает утечки памяти в IE8-: ```js 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` из замыкания: ```js 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); } ``` Теперь циклической ссылки нет -- и не будет утечки. ## Объемы утечек памяти Объем "утекающей" памяти может быть небольшим. Тогда это почти не ощущается. Но так как замыкания ведут к сохранению переменных внешних функций, то одна функция может тянуть за собой много чего ещё. Представьте, вы создали функцию, и одна из ее переменных содержит очень большую по объему строку (например, получает с сервера). ```js function f() { var data = "Большой объем данных, например, переданных сервером" /* делаем что-то хорошее (ну или плохое) с полученными данными */ function inner() { // ... } return inner; } ``` Пока функция `inner` остается в памяти, `LexicalEnvironment` с переменной большого объема внутри висит в памяти. Висит до тех пор, пока функция `inner` жива. Как правило, JavaScript не знает, какие из переменных функции `inner` будут использованы, поэтому оставляет их все. Исключение -- виртуальная машина V8 (Chrome, Opera, Node.JS), она часто (не всегда) видит, что переменная не используется во внутренних функциях, и очистит память. В других же интерпретаторах, даже если код спроектирован так, что никакой утечки нет, по вполне разумной причине может создаваться множество функций, а память будет расти потому, что функция тянет за собой своё замыкание. Сэкономить память здесь вполне можно. Мы же знаем, что переменная `data` не используется в `inner`. Поэтому просто обнулим её: ```js function f() { var data = "Большое количество данных, например, переданных сервером" /* действия с data */ function inner() { // ... } *!* data = null; // когда data станет не нужна - */!* return inner; } ``` ## Поиск и устранение утечек памяти ### Проверка на утечки Существует множество шаблонов утечек и ошибок в браузерах, которые могут приводить к утечкам. Для их устранения сперва надо постараться изолировать и воспроизвести утечку. ### Настройка браузера Утечки могут возникать из-за расширений браузера, взимодействющих со страницей. Еще более важно, что **утечки могут быть следствием конфликта двух браузерных расширений** Например, было такое: память текла когда включены расширения Skype и плагин антивируса одновременно. Чтобы понять, в расширениях дело или нет, нужно отключить их:
  1. Отключить Flash.
  2. Отключить анивирусную защиту, проверку ссылок и другие модули и дополнения.
  3. Отключить плагины. Отключить ВСЕ плагины.
## Инструменты Пожалуй, единственный браузер с поддержкой отладки памяти -- это Chrome. В инструментах разработчика вкладка Timeline -- Memory показывает график использования памяти. Можем посмотреть, сколько памяти и куда он использует. Также в Profiles есть кнопка Take Heap Snapshot, здесь можно сделать и исследовать снимок текущего состояния страницы. Снимки можно сравнивать друг с другом, выяснять количество новых объектов. Можно смотреть, почему объект не очищен и кто на него ссылается. Замечательная статья на эту тему есть в документации: [Chrome Developer Tools: Heap Profiling](http://code.google.com/chrome/devtools/docs/heap-profiling.html). Утечки памяти штука довольно сложная. В борьбе с ними вам определенно понадобится одна вещь: *Удача!*