en.javascript.info/6-optimize/4-memory-leaks/article.md
2015-04-23 12:31:37 +03:00

317 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Утечки памяти
*Утечки памяти* происходят, когда браузер по какой-то причине не может освободить память от недостижимых объектов.
Обычно это происходит автоматически ([](/memory-management)). Кроме того, браузер освобождает память при переходе на другую страницу. Поэтому утечки в реальной жизни проявляют себя в двух ситуациях:
[cut]
<ol>
<li>Приложение, в котором посетитель все время на одной странице и работает со сложным JavaScript-интерфейсом. В этом случае утечки могут постепенно съедать доступную память.</li>
<li>Страница регулярно делает что-то, вызывающее утечку памяти. Посетитель (например, менеджер) оставляет компьютер на ночь включенным, чтобы не закрывать браузер с кучей вкладок. Приходит утром -- а браузер съел всю память <strike>и рухнул</strike> и сильно тормозит.</li>
</ol>
Утечки бывают из-за ошибок браузера, ошибок в расширениях браузера и, гораздо реже, по причине ошибок в архитектуре 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 = '<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"]
Особенности:
<ul>
<li>Если убрать отмеченную строку, то утечки не будет.</li>
<li>Если заменить строку `(*)` на `elem.innerHTML = ''`, то память будет очищена, т.к. этот способ работает по-другому, нежели просто `removeChild` (см. главу [](/memory-management)).</li>
<li>Утечка произойдёт не только при доступе к `rows`, но и к другим свойствам, например `elem.firstChild.tBodies[0]`.</li>
</ul>
Эта утечка проявляется, в частности, при удалении детей элемента следующей функцией:
```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);
}
```
Как вы думаете, почему? Если вы внимательно читали то, что написано выше, то имеете информацию для ответа на этот вопрос..
Посмотрим, какая структура памяти создается при каждом запуске:
<img src="leak-xhr.png">
Когда запускается асинхронный запрос `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);
}
```
<img src="leak-xhr-2.png">
Теперь циклической ссылки нет -- и не будет утечки.
## Объемы утечек памяти
Объем "утекающей" памяти может быть небольшим. Тогда это почти не ощущается. Но так как замыкания ведут к сохранению переменных внешних функций, то одна функция может тянуть за собой много чего ещё.
Представьте, вы создали функцию, и одна из ее переменных содержит очень большую по объему строку (например, получает с сервера).
```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;
}
```
## Поиск и устранение утечек памяти
### Проверка на утечки
Существует множество шаблонов утечек и ошибок в браузерах, которые могут приводить к утечкам. Для их устранения сперва надо постараться изолировать и воспроизвести утечку.
<ul>
<li>**Необходимо помнить, что браузер может очистить память не сразу когда объект стал недостижим, а чуть позже.** Например, сборщик мусора может ждать, пока не будет достигнут определенный лимит использования памяти, или запускаться время от времени.</li>
Поэтому если вы думаете, что нашли проблему и тестовый код, запущенный в цикле, течёт -- подождите примерно минуту, добейтесь, чтобы памяти ело стабильно и много. Тогда будет понятно, что это не особенность сборщика мусора.</li><li>**Если речь об IE, то надо смотреть "Виртуальную память" в списке процессов, а не только обычную "Память".** Обычная может очищаться за счет того, что перемещается в виртуальную (на диск).</li>
<li>Для простоты отладки, если есть подозрение на утечку конкретных объектов, в них добавляют большие свойства-маркеры. Например, подойдет фрагмент текста: `new Array(999999).join('leak')`.</li>
</ul>
### Настройка браузера
Утечки могут возникать из-за расширений браузера, взимодействющих со страницей. Еще более важно, что **утечки могут быть следствием конфликта двух браузерных расширений** Например, было такое: память текла когда включены расширения Skype и плагин антивируса одновременно.
Чтобы понять, в расширениях дело или нет, нужно отключить их:
<ol>
<li>Отключить Flash.</li>
<li>Отключить анивирусную защиту, проверку ссылок и другие модули и дополнения.</li>
<li>Отключить плагины. Отключить ВСЕ плагины.
<ul>
<li>
Для IE есть параметр коммандной строки:
```
"C:\Program Files\Internet Explorer\iexplore.exe" -extoff
```
Кроме того необходимо отключить сторонние расширения в свойствах IE.
<img src="ie9_disable1.png">
<img src="ie9_disable2.png">
</li>
<li>Firefox необходимо запускать с чистым профилем. Используйте следующую команду для запуска менеджера профилей и создания чистого пустого профиля:
```
firefox --profilemanager
```
</li>
</ul>
</li>
</ol>
## Инструменты
Пожалуй, единственный браузер с поддержкой отладки памяти -- это Chrome. В инструментах разработчика вкладка Timeline -- Memory показывает график использования памяти.
<img src="chrome.png">
Можем посмотреть, сколько памяти и куда он использует.
Также в Profiles есть кнопка Take Heap Snapshot, здесь можно сделать и исследовать снимок текущего состояния страницы. Снимки можно сравнивать друг с другом, выяснять количество новых объектов. Можно смотреть, почему объект не очищен и кто на него ссылается.
Замечательная статья на эту тему есть в документации: [Chrome Developer Tools: Heap Profiling](http://code.google.com/chrome/devtools/docs/heap-profiling.html).
Утечки памяти штука довольно сложная. В борьбе с ними вам определенно понадобится одна вещь: *Удача!*
<img src="goodluck.png">