Утечки памяти происходят, когда браузер по какой-то причине не может освободить память от недостижимых объектов.
Обычно это происходит автоматически (Управление памятью в JavaScript). Кроме того, браузер освобождает память при переходе на другую страницу. Поэтому утечки в реальной жизни проявляют себя в двух ситуациях:
- Приложение, в котором посетитель все время на одной странице и работает со сложным JavaScript-интерфейсом. В этом случае утечки могут постепенно съедать доступную память.
- Страница регулярно делает что-то, вызывающее утечку памяти. Посетитель (например, менеджер) оставляет компьютер на ночь включённым, чтобы не закрывать браузер с кучей вкладок. Приходит утром – а браузер съел всю память
и рухнули сильно тормозит.
Утечки бывают из-за ошибок браузера, ошибок в расширениях браузера и, гораздо реже, по причине ошибок в архитектуре 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):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=8">
</head>
<body>
<script>
// Утечка в IE8 standards mode, а также в IE9 в режиме IE8
// См. http://blog.j15r.com/2009/07/memory-leaks-in-ie8.html
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);
}
</script>
<p>Нажимайте на кнопку и наблюдайте, как увеличивается количество занимаемой браузером памяти.</p>
<button onclick="leak()">Создать утечку!</button>
</body>
</html>
Круговая ссылка и, как следствие, утечка может возникать и неявным образом, через замыкание:
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):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=8">
</head>
<body>
<script>
// Утечка в IE8 standards mode, а также в IE9 в режиме IE8
// См. http://blog.j15r.com/2009/07/memory-leaks-in-ie8.html
function leak() {
// Создаём новый DIV, добавляем к BODY
var elem = document.createElement('div');
document.body.appendChild(elem);
elem.__expando = {
bigAss: new Array(1000000).join('lalala'),
method: function() {} // создаётся круговая ссылка через замыкание
};
// Удалить элемент из DOM. Браузер должен очистить память.
elem.parentElement.removeChild(elem);
}
</script>
<p>Нажимайте на кнопку и наблюдайте, как увеличивается количество занимаемой браузером памяти.</p>
<button onclick="leak()">Создать утечку!</button>
</body>
</html>
Без привязки метода 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):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=8">
</head>
<body>
<script>
// Утечка в IE8 standards mode, а также в IE9 в режиме IE8
// См. http://blog.j15r.com/2009/07/memory-leaks-in-ie8.html
function leak() {
// Создаём новый DIV, добавляем к BODY
var elem = document.createElement('div');
document.body.appendChild(elem);
// Записываем в свойство ссылку на объект
var menu = new Menu(elem);
elem.menu = menu;
// Удалить элемент из DOM. Браузер должен очистить память.
elem.parentElement.removeChild(elem);
}
function Menu(elem) {
elem.onclick = function() {};
this.bigAss = new Array(1000000).join('lalala');
}
</script>
<p>Нажимайте на кнопку и наблюдайте, как увеличивается количество занимаемой браузером памяти.</p>
<button onclick="leak()">Создать утечку!</button>
</body>
</html>
Утечка 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):
<!DOCTYPE HTML>
<html>
<body>
<script>
var elem = document.createElement('div'); // любой элемент
// Течёт в настоящем IE8, Standards Mode
// Не течёт в других IE. Не течёт в IE9 в режиме совместимости с IE8
function leak() {
for (var i = 0; i < 2000; i++) {
elem.innerHTML = '<table><tr><td>1</td></tr></table>';
elem.firstChild.rows[0]; // просто доступ через rows[] приводит к утечке
// при том, что мы даже без сохраненяем значение в переменную
elem.removeChild(elem.firstChild); // удалить таблицу
// elem.innerHTML = '' очистил бы память, он работает по-другому, см. главу "управление памятью"
}
}
</script>
<p>Нажимайте на кнопку и наблюдайте, как увеличивается количество занимаемой браузером памяти.</p>
<button onclick="leak()">Создать утечку!</button>
</body>
</html>
Особенности:
- Если убрать отмеченную строку, то утечки не будет.
- Если заменить строку
(*)
наelem.innerHTML = ''
, то память будет очищена, т.к. этот способ работает по-другому, нежели простоremoveChild
(см. главу Управление памятью в JavaScript). - Утечка произойдёт не только при доступе к
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):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<p>Страница создаёт объект <code>XMLHttpRequest</code> каждые 50 мс.</p>
<p>Нажмите на кнопку и смотрите на память, она течёт в IE<9.</p>
<button onclick="setInterval(leak, 50);">Запустить</button>
<script>
function leak() {
var xhr = new XMLHttpRequest();
xhr.open('GET', '?' + Math.random(), true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
document.getElementById('test').innerHTML++;
}
}
xhr.send(null);
}
</script>
<div>Количество запросов: <span id="test">0</span></div>
</body>
</html>
Чтобы это исправить, нам нужно разорвать круговую ссылку 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 и плагин антивируса одновременно.
Чтобы понять, в расширениях дело или нет, нужно отключить их:
- Отключить Flash.
- Отключить антивирусную защиту, проверку ссылок и другие модули, и дополнения.
- Отключить плагины. Отключить ВСЕ плагины.
- Для 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.
Утечки памяти штука довольно сложная. В борьбе с ними вам определённо понадобится одна вещь: Удача!