4 типи витоків пам'яті в JavaScript і як їх позбутися 1/2

b
Дізнайтеся про витоки пам’яті в JavaScript і що ви можете зробити, щоб їх виправити!
У цій статті ми розглянемо типології витоку пам’яті в коді JavaScript на стороні клієнта. Ми також дізнаємося, як використовувати інструменти розробника Chrome, щоб знайти їх. Давайте розпочнемо!

Вступ

Витоки пам’яті є проблемою, з якою стикаються всі розробники, навіть при роботі з мовами, які керують пам’яттю, бувають випадки, коли виникають витоки пам’яті. Витоки є причиною цілого сімейства проблем: уповільнення, збої, високий час відгуку і навіть проблеми з іншими програмами.

Що таке витік пам’яті?

Витік пам’яті по суті можна визначити як область пам’яті, яка більше не використовується програмою, але яка не звільняється і, отже, не стає доступною для ОС. У мовах є різні методи управління пам'яттю. Ці методи зменшують ймовірність витоку пам’яті. Тим не менш, знати, чи стала область пам’яті непотрібною чи ні, є потенційно нерозв’язною проблемою. Тільки дизайнери можуть уточнити, чи не використовується область пам’яті. У Вікіпедії є гарні статті про ручне та автоматичне керування пам’яттю.

Управління пам'яттю в JavaScript

JavaScript є однією з мов зі збіркою сміття. Мови збирання сміття допомагають розробникам керувати пам’яттю, періодично перевіряючи, яка раніше виділена область пам’яті повинна залишатися доступною для інших частин програми. Іншими словами, мови на основі збирання сміття зменшують проблему керування пам’яттю з «яка область пам’яті все ще потрібна» до «яка область пам’яті залишається доступною для решти програми». Різниця незначна, але важлива: хоча тільки розробник знає, яка область пам’яті повинна залишатися доступною в майбутньому, області пам’яті, які стали недоступними, можна алгоритмічно ідентифікувати та позначити для повернення в ОС.

Мови без сміття зазвичай використовують інші методи керування пам’яттю: явне керування пам’яттю, коли розробники повідомляють компілятору, коли область пам’яті більше не потрібна, і підрахунок посилань, при якому кількість використання пов’язана з кожним блоком пам’яті (коли досягає нуля, область повертається в ОС). Ці методи мають свої аналоги (і потенційні ризики витоку).

Витоки в JavaScript

Основною причиною витоків у мовах збирання сміття є небажані посилання. Щоб зрозуміти, що таке небажані посилання, ми повинні спочатку зрозуміти, як збирач сміття визначає, доступна область пам’яті чи ні.

«Основна причина витоків у мовах на основі збирача сміття — небажані посилання»

 

Позначте і змітайте

Більшість збирачів сміття використовують алгоритм, який називається mark-and-sweep. Алгоритм складається з наступних кроків:

  1. Збірник сміття створює список «коренів». Корені - це зазвичай глобальні змінні, на які посилаються в коді. У JavaScript об’єкт «windows» є прикладом глобальної змінної, яка може виконувати роль кореня. Об'єкт Windows завжди присутній, тому збирач сміття може вважати його - і всіх його дочірніх - завжди присутнім (тобто, не сміття).
  2. Усі корені перевіряються та позначаються як активні. Усі діти також проходять рекурсивний огляд. Все, до чого можна отримати доступ із root, не вважається сміттям.
  3. Усі області пам’яті, які не позначені як активні, можна вважати сміттям. Потім колектор може звільнити ці області пам’яті та повернути їх до ОС.

Більшість сучасних збирачів сміття побудовано на цьому алгоритмі з варіаціями, але принцип залишається тим самим: усі доступні області пам’яті позначаються як такі, а решта вважається сміттям.
Небажані посилання – це посилання на області пам’яті, які розробник, як відомо, більше не використовують, але які з цілого ряду причин залишаються в дереві активного кореня. У контексті JavaScript небажані посилання — це змінні, які зберігаються десь у коді, які більше не використовуватимуться, і вказують на область пам’яті, яку можна було б звільнити. Для деяких це помилка розробника.
Щоб зрозуміти, які є найпоширенішими витоками в JavaScript, ви повинні подивитися, як посилання часто забувають.
a

Чотири типи витоків JavaScript:

1: випадково глобальні змінні

Однією з цілей JavaScript була розробка мови, яка була схожа на Java, але була достатньо дозволеною, щоб її могли використовувати початківці. Це відображається в тому, як JavaScript обробляє неоголошені змінні: посилання на неоголошену змінну створює нову змінну в глобальному об’єкті. У випадку браузера глобальним об’єктом є window. Іншими словами:
function foo(arg) {
bar = "this is a hidden global variable";
}
Is in fact:
function foo(arg) {
window.bar = "this is an explicit global variable";
}
Якщо передбачалося, що бар повинен містити посилання на змінну лише в межах функції foo, і ви забули її оголосити, глобальна змінна створюється випадково. У цьому прикладі витік рядка не дуже небезпечний, але може бути набагато гіршим.
Інший спосіб випадкового створення глобальної змінної:
function foo() {
this.variable = "potential accidental global";
}
// Foo called on its own, this points to the global object (window)
// rather than being undefined.
foo();

Щоб запобігти появі цих помилок, додайте 'use strict'; на початку ваших файлів JavaScript. Це запускає суворіший режим аналізу JavaScript, який випадково запобігає глобалізації.

Примітка щодо глобальних змінних
Незважаючи на те, що ми говоримо про непередбачувані глобальні значення, реальність така, що багато коду всіяно явними глобальними змінними. Вони за визначенням не підлягають стягненню (якщо вони не анульовані або перепризначені). Зокрема, викликають занепокоєння глобальні змінні, які використовуються для зберігання та обробки великих обсягів інформації. Якщо вам потрібно використовувати глобальні змінні для зберігання великої кількості даних, обов’язково обнуліть їх або перепризначте їх, коли закінчите.
Поширеною причиною збільшення споживання пам’яті у зв’язку з Globals є кешування. Кеш зберігає дані, які використовуються неодноразово. Щоб це було ефективним, кеш повинен мати більший ліміт розміру. Кеші, які безмежно збільшуються, призводять до високого споживання пам’яті, оскільки вміст не може бути зібрано.

2: забуті таймери або зворотні виклики

Використання setInterval досить поширене в JavaScript. Інші бібліотеки надають спостерігачі та інші системи, які підтримують зворотні виклики. Більшість із цих бібліотек роблять зворотні виклики недоступними після того, як самі їхні екземпляри стають недоступними. У випадку setInterval зазвичай можна побачити такий код:
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// Do stuff with node and someResource.
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
Цей приклад ілюструє, що може статися з висячими таймерами: таймерами, які посилаються на вузол або дані, які більше не потрібні. Об’єкт, представлений вузлом, може бути видалений у майбутньому, що зробить весь блок всередині обробника діапазону непотрібним.
Однак обробник, як і інтервал, все ще активний і тому не може бути зібраний (для цього інтервал потрібно було б зупинити). Якщо діапазон не може бути зібраний, не можна також зібрати його залежності. Це означає, що ресурси, які зберігають потенційно досить значні дані, також не можуть бути зібрані.
У випадку спостерігачів важливо зробити виклики явними, щоб видалити їх, коли вони більше не потрібні (або пов’язані об’єкти ось-ось стануть недоступними). У минулому це було особливо важливо, оскільки деякі браузери (IE6) не могли добре обробляти циклічні посилання (додаткову інформацію про це див. нижче).
Сьогодні більшість браузерів можуть і збиратимуть обробники спостерігачів, як тільки спостережуваний об’єкт стає недоступним. Однак перед видаленням об’єкта бажано видаляти спостерігачів. Наприклад :
var element = document.getElementById('button');
function onClick(event) {
element.innerHtml = 'text';
}
element.addEventListener('click', onClick);
// Do stuff
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers that don't
// handle cycles well.
Примітка про спостерігачі об’єктів і циклічні посилання
Спостерігачі та циклічні посилання вже давно є кошмаром для розробників JavaScript. Це сталося через помилку (або дизайнерське рішення) у збірнику сміття Internet Explorer. Старіші версії IE не могли виявити циклічні посилання між вузлами DOM і кодом JavaScript. Це характерно для спостерігачів, які зазвичай зберігають посилання на спостережуваний (як у прикладі вище). Іншими словами, кожен раз, коли спостерігач додається до вузла в Internet Explorer, це призводить до витоку пам’яті. Ось чому розробники почали вручну видаляти обробники перед вузлами або обнуляти посилання в спостерігачах. Сьогодні сучасні браузери (включаючи 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() {
image.src = 'http://some.url/image';
button.click();
console.log(text.innerHTML);
// Much more logic
}
function removeButton() {
// The button is a direct child of body.
document.body.removeChild(document.getElementById('button'));
// At this point, we still have a reference to #button in the global
// elements dictionary. In other words, the button element is still in
// memory and cannot be collected by the GC.
}
Ще одна річ, яку слід враховувати, це посилання на внутрішні вузли або листки дерева DOM. Припустимо, наприклад, ви зберігаєте посилання на конкретну клітинку таблиці (a тег) у вашому коді JavaScript. У якийсь момент ви вирішите видалити конкретну таблицю з DOM, але залишити посилання на цю клітинку. Інтуїтивно можна було б подумати, що ГХ правильно збере все, крім цієї комірки. На практиці цього не відбудеться: ця комірка є дочірнім вузлом масиву і діти зберігають посилання на своїх батьків. Це означає, що вся таблиця залишиться в пам’яті через посилання JavaScript на цю клітинку. Враховуйте це, якщо ви зберігаєте посилання на елементи 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);
Цей фрагмент робить одну річ: будь-коли замінити річ називається, річ отримує новий об’єкт, який містить великий масив і нове закриття (деякийМетод). У той же час невикористана змінна містить паркан, на який є посилання оригінальна річ (річ від попереднього дзвінка до замінити річ). Вже трохи складно, правда? Важливо те, що після створення області дії для огорож, які знаходяться в одній батьківській області, ця область є спільною.
У цьому випадку область, створена для закриття деякийМетод ділиться з невикористаний. Невикористаний має посилання на оригінальна річ. Хоча невикористаний ніколи не використовуватися. деякийМетод можна використовувати через річ. І подобається деякийМетод поділяє сферу застосування огорожі с невикористаний, навіть якщо невикористаний ніколи не використовується, його посилання на оригінальна річ змушує його залишатися активним (запобігає його збору).
Коли цей фрагмент виконується багаторазово, ми бачимо постійне збільшення використання пам’яті. І не зменшується з проходженням ГХ. По суті, створюється закриваючий зв’язаний список (з коренем змінної річ) і кожна область цих замикань містить посилання на великий масив, який генерує наслідок витоку.
Це артефакт реалізації. Інша реалізація огорож, яка б вирішила цю проблему, можлива, як пояснюється в метеор-блог.
Оригінальна стаття de Себастьян Пейротт переклад JS Staff
 
Не пропустіть продовження наступного епізоду: як усунути витік пам’яті 🙂
[separator type=”” size=”” icon=”star”] [actionbox color=”default” title=”” description=”JS-REPUBLIC – сервісна компанія, що спеціалізується на розробці JavaScript. Ми є затвердженим навчальним центром. Знайдіть всю нашу технічну підготовку на нашому партнерському сайті, присвяченому навчанню” btn_label=”Наше навчання” btn_link=”http://training.ux-republic.com” btn_color=”primary” btn_size=”big” btn_icon=”star” btn_external =”1″]