В этой статье мы рассмотрим распространённые виды утечек памяти в клиентском JavaScript. Также мы узнаем, как их обнаружить с помощью Chrome Development Tools.


timeline в Chrome Dev Tools


Примечание переводчика: первая часть статьи насыщена примечаниями переводчика. В ходе анализа материала стало понятно, что некоторые моменты стоит отдельно пояснить.


Вступление


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


Что такое утечка памяти?


Утечка памяти — память, которая больше не требуется приложению, но по какой-то причине не возвращается операционной системе или пулу доступной памяти (примечание переводчика: в кучу). Языки программирования используют разные подходы, снижающие риск возникновения утечек памяти, однако сама задача о том, понадобится ли ещё определенный фрагмент памяти или нет, алгоритмически неразрешима (примечание переводчика: она сводится к проблеме остановки). Иными словами, только разработчик может определить, возможно ли вернуть определенный фрагмент памяти операционной системе. Управление памятью в языках программирования делится на ручное и автоматическое. Первый тип предоставляет разработчику набор инструментов, помогающих напрямую взаимодействовать с памятью. Во втором существует специальный процесс, называемый «сборщиком мусора» (англ. garbage collector), вызываемый автоматически и удаляющий память.


Примечание переводчика: более подробно это описано в Википедии: сборка мусора, manual memory management, garbage collection.


Управление памятью в JavaScript


JavaScript — язык программирования со встроенным сборщиком мусора. Сборщик периодически проверяет, какие из выделенных приложению фрагментов памяти остаются «достижимы» из различных частей этого приложения. Иными словами, сборщик мусора переводит вопрос «какая память до сих пор нужна?» в вопрос «к какой памяти можно обратиться?». Разница кажется незначительной, однако это не так: хотя лишь разработчик знает, потребуется ли фрагмент выделенной памяти в будущем или нет, недостижимую память можно вычислить алгоритмически и пометить к возвращению в ОС.


Языки, не имеющие сборщиков мусора, работают по другим принципам. Например, существует явное управление памятью: разработчик напрямую говорит компилятору, что данный фрагмент памяти можно удалить. Также существует алгоритм подсчёта ссылок, при котором с каждым блоком памяти ассоциируется количество его использований (и когда оно обнулится, блок возвращается в ОС). Эти техники имеют свои плюсы и минусы, и могут привести к утечкам памяти.


Примечание переводчика: алгоритм подсчёта ссылок используется и в сборщиках мусора. Кроме того, стоит заметить, что работа этого алгоритма в базовом виде может привести к возникновению циклических ссылок, при котором неиспользуемые объекты ссылаются друг на друга, взаимно блокируя удаление. Подробнее — в Википедии.


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


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


Алгоритм пометок (Mark-and-sweep)


Большинство сборщиков мусора используют алгоритм пометок (mark-and-sweep):


  1. Сборщик мусора строит список «корневых объектов», или «корней». Как правило ими становятся объявленные в коде глобальные переменные. В JavaScript типичный корень — объект window. Так как window существует на протяжении всей работы страницы, сборщик мусора поймёт, что этот объект и его потомки всегда будут присутствовать в среде исполнения программы (т.е. не станут мусором).


  2. Сборщик рекурсивно обходит корни и их потомков, помечая их как активные (т.е. не мусор). Всё, до чего можно добраться из корня, не рассматривается в качестве мусора.


  3. После второго шага фрагменты памяти, не помеченные как активные, могут считаться мусором. Теперь сборщик может освободить эту память и вернуть в ОС.

Современные сборщики мусора улучшают этот алгоритм, но его суть остаётся прежней: пометить достижимые фрагменты памяти, а остальное объявить мусором. Теперь можно дать определение нежелательным ссылкам — это ссылки, достижимые из корня, но ссылающиеся на фрагменты памяти, которые точно никогда больше не понадобятся. В JavaScript нежелательными ссылками станут потерявшие актуальность переменные, забытые в коде, удерживающие в памяти ненужные более объекты. Кстати, некоторые считают, что это ошибки разработчиков, а не языка.


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


Примечание переводчика: перед тем, как читать дальше, рекомендую посмотреть статью MDN об управлении памятью, более подробно раскрывающую тему.


Четыре самых распространённых вида утечек памяти в JavaScript


Примечание переводчика: здесь и далее в примерах мы будем рассматривать клиентский JavaScript. Будет считать без излишних уточнений и оговорок, что у нас есть глобальный объект window. В иной среде исполнения JavaScript window может не быть, однако обсуждаемые в статье причины утечек будут актуальны.


1: Случайные глобальные переменные


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


function foo(arg) {
    bar = "скрытая глобальная переменная";
}

На самом деле он означает:


function foo(arg) {
    window.bar = "явно объявленная глобальная переменная";
}

Если вы хотите, чтобы bar содержала ссылку на переменную лишь внутри области видимости функции foo, но забыли указать в объявлении var, то будет создана глобальная переменная. В данном случае утечку памяти создаст простая строка. Много вреда это не причинит, но, конечно, ситуация могла бы быть намного хуже.


Ещё один способ создать случайную глобальную переменную — использовать this:


function foo() {
    this.variable = "potential accidental global";
}

// Если foo вызвать саму по себе, this будет указывать 
// на глобальный объект (window), 
// вместо того, чтобы быть undefined.
foo();

Чтобы избежать подобных ошибок, добавляйте 'use strict'; в начало JavaScript-файлов. Это директива, включающая строгий режим парсинга JavaScript, препятствующий возникновению случайных глобальных переменных.


Замечание о глобальных переменных


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


Примером увеличенного расхода памяти, связанным с глобальными переменными, являются кэши — объекты, которые сохраняют повторно используемые данные. Для эффективной работы их следует ограничивать по размеру. Если кэш увеличивается без ограничений, он может привести к высокому расходу памяти, поскольку его содержимое не может быть очищено сборщиком мусора.


2: Забытые таймеры и коллбэки


Довольно часто встречается подобное использование функции setInterval:


var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // Сделаем что-нибудь с node и someResource.
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

Этот пример показывает, чем вредны подвисшие таймеры (обращающиеся к уже не нужным узлам или данным). Если удалить узел, весь блок внутри функции-обработчика станет ненужным. Но, до тех пор, пока setInterval активен, обработчик тоже активен и не может быть очищен сборщиком мусора (ведь для этого надо сперва остановить интервал). А значит, его зависимости тоже не могут быть удалены из памяти. Получим, что someResource, хранящий, вероятно, большой объём данных, не может быть очищен сборщиком мусора.


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


Рассмотрим теперь ситуацию с обработчиками событий. Обработчики следует удалять, когда они становятся не нужны, или ассоциированные с ними объекты становятся недоступны. В прошлом это было критично, так как некоторые браузеры (Internet Explorer 6) не умели грамотно обрабатывать циклические ссылки (см. заметку ниже). Большинство современных браузеров удаляет обработчики событий, как только объекты становятся недостижимы. Однако по-прежнему правилом хорошего тона остаётся явное удаление обработчиков событий перед удалением самого объекта. Например:


var element = document.getElementById('button');

function onClick(event) {
    element.innerHtml = 'text';
}

element.addEventListener('click', onClick);
// Какие-то действия.
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// Теперь, когда элемент удаляется из области видимости,
// сборщиком будут очищены и сам элемент, и onClick.
// Даже если код работает в старом браузере, 
// не умеющем правильно обрабатывать такие циклы.

Заметка об обработчиках событий и циклических ссылках


Обработчики событий и циклические ссылки издавна считались проблемой JavaScript-разработчиков. Это было связано с ошибкой (или дизайнерским решением) сборщика мусора в Internet Explorer. Старые версии Internet Explorer не могли обнаружить циклические ссылки между DOM-элементами и JavaScript кодом. Добавим к этому, что в обработчиках событий обычно содержится ссылка на объект события (как в примере выше). Это означает, что каждый раз, когда в Internet Explorer на DOM-узел добавлялся слушатель, возникала утечка памяти. Поэтому веб-разработчики начали явно удалять обработчики событий до удаления DOM-узлов или обнулять ссылки внутри обработчиков. Современные браузеры (включая Internet Explorer и Microsoft Edge) используют алгоритмы, находящие циклические ссылки и правильно их обрабатывающие. Теперь не обязательно вызывать removeEventListener перед удалением узла.


Фреймворки и библиотеки, такие, как jQuery, убирают обработчики перед тем, как удалить сам узел, если для их создания использовался библиотечный API. Это делается самими библиотеками и гарантирует отсутствие утечек, даже при работе с проблемными браузерами, вроде старого Internet Explorer.


3: Ссылки на удалённые из DOM элементы


Иногда полезно хранить DOM-узлы внутри структур данных. Предположим, вы хотите точечно обновить содержимое нескольких строк в таблице. Имеет смысл сохранить ссылку на каждый DOM-ряд в словаре или массиве. В этом случае на один и тот же DOM-элемент будут указывать две ссылки — одна в DOM-дереве, а вторая в словаре. Если в будущем вы решите удалить эти строки, вам понадобится сделать и ту, и другую ссылку недостижимыми.


var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};

function doStuff() {
    elements.image.src = 'http://some.url/image';
    elements.button.click();
    console.log(elements.text.innerHTML);
    // Остальная логика.
}

function removeButton() {
    // Кнопка находится непосредственно в body.
    document.body.removeChild(document.getElementById('button'));

    // В этом случае мы всё равно ссылаемся на #button 
    // в глобальном объекте elements. 
    // Т.е. кнопка до сих пор находится в памяти 
    // и не может быть удалена сборщиком мусора.
}

В дополнении к этому нужно что-то делать со ссылками на внутренние узлы DOM-дерева. Предположим, что мы храним в коде ссылку на какую-то конкретную ячейку таблицы (на тег <td>). В будущем мы решаем убрать таблицу из DOM, но сохранить ссылку на эту ячейку. Интуитивно мы ожидаем, что сборщик мусора очистит всё, кроме этой ячейки. Однако на практике будет иначе: так как ячейка является узлом-потомком таблицы, она хранит ссылки на своих родителей. Получится, что ссылка на ячейку таблицы заставит хранить в памяти всю таблицу. Учтите это, когда сохраняете ссылки на DOM-элементы.


4: Замыкания


Основополагающей частью JavaScript являются замыкания: функции, получающие переменные из родительских областей видимости. Разработчики Meteor обнаружили ситуацию, при которой из-за особенностей реализации среды исполнения JavaScript можно создать утечку памяти подобным хитрым способом:


var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing)
      console.log("hi");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};
setInterval(replaceThing, 1000);

Этот пример делает одну простую вещь: каждый раз, когда вызывается replaceThing, theThing получает новый объект, содержащий большую строку и новое замыкание (someMethod). При этом переменная unused содержит замыкание, ссылающееся на originalThing (а это theThing из предыдущего вызова replaceThing). Уже кое-что смущает, да?


Важно отметить, что, так как область видимости создаётся для замыканий, находящихся всё в той же самой родительской области видимости, эта область видимости будет общей. В этом случае область видимости, создаваемая для замыкания someMethod, разделяется вместе с unused. unused хранит ссылку на originalThing. Хотя unused и не используется, someMethod может быть использован в theThing. Так как someMethod разделяет область видимости с unused, даже если unused никогда не используется, его обращение к originalThing заставляет его всегда оставаться активным (то есть защищает от сборщика мусора).


При работе этого кода можно наблюдать постоянное увеличение используемой памяти. Объём памяти не уменьшается даже когда в дело вступает сборщик мусора. По сути у нас создаётся список связанных замыканий (с корнем в виде переменной theThing), и в каждой области видимости этих замыканий содержится прямая ссылка на большую строку, что представляет собой значительную утечку памяти. Это артефакт реализации. С иной реализацией замыканий потенциально можно обработать эту ситуацию, что и объясняется в блоге Meteor-а.


Неочевидное поведение сборщиков мусора


Хотя сборщики мусора и полезны, у них есть свои недостатки, одним из которых является недетерминированность. Это значит, что сборщики мусора непредсказуемы — обычно невозможно определить, когда будет произведена сборка мусора. Как следствие, иногда программа занимает больше памяти, чем требуется. Также могут наблюдаться кратковременные паузы, что будет особенно заметно в быстро реагирующих на действия программах.


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


  1. Возникло значительное множество выделений памяти.
  2. Большинство элементов (возможно и все) были помечены, как недостижимые (например, мы присвоили ссылкам на ненужный кэш значение null).
  3. Последующих выделений памяти не производится.

В этом случае большинство сборщиков мусора не будет производить дальнейших действий. Иными словами, хоть и существуют недостижимые ссылки, которые можно обработать, сборщик мусора их не затронет. За счёт таких незначительных утечек приложение будет расходовать больше памяти, чем нужно. Google привели отличный пример подобного поведения — JavaScript Memory Profiling docs, example #2.


Обзор инструментов профилирования в Chrome


Chrome предоставляет набор инструментов для профилирования расхода памяти в JavaScript. Для работы с памятью предназначены два важнейших инструмента: вкладка timeline и вкладка профилей.


Вкладка timeline


Вкладка timeline


Вкладка timeline неоценима для обнаружения необычного поведения памяти. При поиске больших утечек обратите внимание на периодические скачки, незначительно уменьшающиеся после сборки мусора. На скриншоте видно непрерывный рост вызывающих утечку памяти объектов. Даже после большой зачистки в конце, общее количество занимаемой памяти больше, чем вначале. Также возрастает количество DOM-узлов. Всё указывает на то, что в коде утечка, связанная с DOM-узлами.


Вкладка профилей


Вкладка профилей


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


Общий список предоставляет обзор разных типов связанных объектов и совокупность их размеров: shallow size (поверхностный размер, сумму всех объектов конкретного типа) и retained size (удерживаемый размер, поверхностный размер плюс размер других объектов, связанных с данным). Также это даёт нам представление о том, насколько далёк объект от своего корня (поле distance).


Список сравнения предоставляет нам ту же информацию и даёт возможность сопоставить разные снапшоты. Это особенно важно для поисков утечек памяти.


Пример: Ищем ошибки с помощью Chrome


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


Рассмотрим код из примеров в документации Chrome:


var x = [];

function createSomeNodes() {
    var div,
        i = 100,
        frag = document.createDocumentFragment();
    for (;i > 0; i--) {
        div = document.createElement("div");
        div.appendChild(document.createTextNode(i + " - "+ new Date().toTimeString()));
        frag.appendChild(div);
    }
    document.getElementById("nodes").appendChild(frag);
}
function grow() {
    x.push(new Array(1000000).join('x'));
    createSomeNodes();
    setTimeout(grow,1000);
}

Функция grow при вызове начнёт создавать узлы <div> и добавлять их в DOM. Также она свяжет с ними большую строку и присоединит её к массиву, созданному в глобальной области видимости. Это вызовет устойчивый прирост памяти, который можно обнаружить с помощью рассмотренных нами инструментов.


Для языков со сборщиками мусора характерны колебания в графике работы памяти. Это ожидаемо, если распространение памяти циклично, как обычно и случается. Мы рассмотрим периодические увеличения памяти, которые не возвращаются к исходному состоянию после отработки сборщика мусора.


Как обнаружить периодическое увеличение памяти


Для этого нам понадобится вкладка timeline. Откройте пример в Chrome, откройте Dev Tools, выберите timeline, выберите memory и нажмите на запись. Затем перейдите на страницу и нажмите The Button. Начнётся утечка памяти. Через какое-то время остановите запись и посмотрите на результаты.


Пример timeline


Этот пример продолжит создавать утечки памяти каждую секунду. После остановки записи установите брейкпоинт в функцию grow, чтобы скрипт остановился и не дал Chrome закрыть страницу. На этом скриншоте два больших признака утечек памяти: график узлов (nodes, зелёная линия) и график JavaScript-кода (синяя линия). DOM-узлы неизменно увеличиваются и никогда не уменьшаются. Это повод задуматься.


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


Итак, теперь ясно, что у нас утечка. Давайте найдём её.


Сделайте два снапшота


Чтобы найти утечку, переместимся в раздел profile. Чтобы объём памяти можно было контролировать, перезагрузите страницу. Нам понадобится функция Take Heap Snapshot.


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


Пример snapshot 1


Есть два способа отследить распространение памяти в промежутке между двумя снапшотами. Можно выбрать Summary и затем кликнуть правой кнопкой на Objects allocated between Snapshot 1 and Snapshot 2 или вместо Summary выбрать Comparison. В обоих случаях мы увидим список объектов, возникших между двумя снимками.


В этом примере довольно просто обнаружить утечки: они большие. Обратите внимание на Size Delta конструктора (string). 8 MB и 58 новых объектов. Выглядит подозрительно: новые объекты возникают, но не удаляются, занимая 8 MB.


Если мы откроем список выделений памяти для конструктора (string), мы увидим несколько значительных выделений среди прочих. Большие значения настораживают. Если мы выберем какое-то одно, увидим кое-что интересное в секции retainers.


Пример snapshot 2


Мы видим, что выбранное выделение памяти является частью массива. В свою очередь, на массив ссылается переменная x из глобальной области видимости. Это означает, что существует путь из выбранного объекта до корня — значит, объект нельзя очистить сборщиком мусора. Мы нашли потенциальную утечку. Отлично. Но пример был прост — редко встречаются настолько большие увеличения расхода памяти. Также видны утечки DOM-узлов, занимающие меньший объём. С помощью этих снапшотов легко обнаружить эти узлы, однако сделать это на больших сайтах будет труднее. Chrome предоставляет дополнительный инструмент, лучше всего справляющийся с данной задачей — функцию Record Heap Allocations.


Ищем утечки с помощью Record Heap Allocations


Если вы установили брейкпоинт, уберите его, позволив скрипту работать дальше. Вернитесь в панель профилей в инструментах разработчика. Нажмите Record Allocation Timeline. Во время работы этого инструмента вы увидите синие скачки в графике сверху, отображающие выделение памяти. Каждую секунду в коде происходит большое выделение памяти. Позвольте скрипту отработать несколько секунд, затем остановите его (не забывайте устанавливать брейкпоинты, иначе Chrome займёт всю память).


обзор Record Heap Allocations


На снимке видно, чем хорош этот инструмент: можно выбрать отрезок таймлайна и посмотреть, какие выделения памяти произошли за данный период. Постараемся максимально приблизить один из скачков. В списке будут показаны только три конструктора: один из них связан с большими утечками ((string)), следующий — с выделением памяти для DOM-узлов, а последний — конструктор Text (создающий содержимое DOM-узла).


Выберите один из конструкторов HTMLDivElement из списка и нажмите Allocation stack.


Record Heap Allocations — выбранный элемент


Бинго! Теперь мы знаем, где был расположен тот элемент (grow -> createSomeNodes). Если внимательно посмотреть на каждый скачок в графике, мы увидим, что конструктор HTMLDivElement вызывается очень часто. Если мы опять перейдём в панель сравнения снапшотов, мы увидим, что этот конструктор вызывает множество выделений памяти, но не удаляет её. Получается, что постоянно увеличивается расход памяти, но сборщик мусора не может очистить даже малую её часть. Налицо все признаки утечки. К тому же, теперь нам известно, какие именно объекты требуют выделения памяти (функция createSomeNodes). Можно смело идти в код и исправлять ошибку.


Ещё одно полезное свойство


Примечание переводчика: чтобы использовать это свойство, откройте Dev Tools -> Settings и включите "record heap allocation stack traces".


Также можно выбрать режим Allocation вместо Summary:


Список выделения памяти


Этот режим выводит список функций и связанный с ними объём памяти. Сразу бросается в глаза grow и createSomeNodes. Если выбрать grow, мы увидим вызываемые им конструкторы объектов. Тут будут (string), HTMLDivElement и Text, которые, как мы уже знаем, вызывают утечку памяти.


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


Дополнительные материалы


Memory Management — Mozilla Developer Network
JScript Memory Leaks — Douglas Crockford (old, in relation to Internet Explorer 6 leaks)
JavaScript Memory Profiling — Chrome Developer Docs
Memory Diagnosis — Google Developers
An Interesting Kind of JavaScript Memory Leak — Meteor blog
Grokking V8 closures


Заключение


Даже в языках со встроенными сборщиками мусора, таких, как JavaScript, возникают утечки памяти. Иногда они могут остаться незамеченными, а иной раз приводят к катастрофе. Важно понимать, как работает управление памятью. Существуют специальные инструменты для анализа распространения памяти и поиска ошибок. Профилирование памяти должно стать частью цикла разработки, особенно для приложений средних и больших размеров. Начните это делать, чтобы не усложнять жизнь вашим пользователям.




Оригинал: 4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them, автор: Sebastian Peyrott.


Перевод: aalexeev, редактура: iamo0, jabher, spearance, zeckson, Наталья Ёркина, Чайка Чурсина.

Поделиться с друзьями
-->

Комментарии (15)


  1. tiagat
    06.09.2016 14:24
    +1

    Спасибо за статью! Как раз вовремя.


    1. tiagat
      06.09.2016 18:27

      Что я не так сказал?


  1. Buscando
    06.09.2016 14:34
    -1

    Отличная статья, спасибо!


  1. Miwwa
    06.09.2016 15:07
    +2

    Самая полная и подробная статья про утечки памяти в JS, которую я видел. Огромное спасибо


  1. MattLe
    06.09.2016 15:07

    Отличная статья, большое спасибо!!!


  1. baldr
    06.09.2016 22:13
    +1

    Может ли кто-то дать ссылки или советы по поиску утечек в проекте с jQuery и событиями?
    Я стараюсь работать, в основном, с бэкендом и на фронтенд не очень много сил трачу. Использую CoffeeScript+jQuery и некоторые плагины.
    Вижу что в FF память течет — после перезагрузки страницы объем занятой памяти (плагин TabMemoryUsage) увеличивается стабильно на 3Мб. Профилирование javascript — не такая простая задача, как для Python — покопался в отладочных инструментах, но не все так очевидно как на примере из статьи.
    Есть еще какие-то рекомендации?


  1. vanxant
    07.09.2016 00:58

    Краткое содержание, если опустить специально сконструированные хаки для js:
    1. «use strict», Luke
    2. Не быдлокодь


  1. barkadron
    07.09.2016 08:09

    Огромное спасибо, очень интересно!
    Остался вопрос — сборщик мусора запускает алгоритм поиска и пометки недостижимых ссылок с какой-то периодичностью или по какому-то событию? Как часто это происходит, примерно?


    1. aalexeev
      07.09.2016 08:10

      Как указано в статье, работа сборщика недетерменирована. Т.е. мы не можем запустить его из JS или угадать, когда он сработает.


      1. barkadron
        07.09.2016 09:16

        Да, это я понимаю, но ведь есть какой-то алгоритм его запуска.
        В статье сказано, что сборка мусора может не произойти, если все недостижимые ссылки будут найдены, но не происходит последующих выделений памяти — тут всё понятно. А как на счёт самого процесса поиска недостижимых ссылок — как часто это происходит? При новых выделениях памяти, через фиксированный интервал времени или как-то иначе?


        1. barkadron
          07.09.2016 10:42
          +1

          Нашёл статью http://v8project.blogspot.ru/2015/08/getting-garbage-collection-for-free.html
          Грубо говоря, выходит, что у Хрома есть 16.6ms-циклы, и если он успевает сделать приоритетные задачи до окончания итерации, то остаток времени расходуется на низко-приоритетные задачи, включая и сборку мусора.


      1. f0rk
        13.09.2016 15:03
        +2

        В Chrome есть флажек --js-flags="--expose-gc", с ним в коде можно вызывать gc(), бывает полезно при ловле утечек.


  1. Sky-Fox
    07.09.2016 09:21

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


  1. bkzhn
    08.09.2016 12:39

    Хорошая статья.


  1. zeezdev
    13.09.2016 17:17
    +1

    Спасибо за статью!
    Сказанное о каллбеках на примере рекомендуемого к использованию addEventListener справедливо так же и для attachEvent / detachEvent?
    Как правильно поступать перед удалением объекта при простом «навешивании» обработчиков в стиле:

    element.onclick = function() { alert(1); }
    

    Делать
    element.onclick = null;