Витоки пам'яті в JavaScript: як їх позбутися (2/2)

Ця стаття є продовженням статті «4 види витоків пам'яті»
c

Неінтуїтивна поведінка збирачів сміття

Хоча збирачі сміття дуже практичні, вони мають свою частку компромісів. Одним із таких компромісів є їхня недетермінованість. Іншими словами, збирачі сміття непередбачувані. Як правило, неможливо з упевненістю визначити, коли буде здійснюватися збір сміття. Це означає, що в деяких випадках програма використовує більше пам’яті, ніж необхідно. В інших випадках можна помітити короткі перерви у чутливих програмах. Хоча недетермінований означає, що ніколи не можна бути впевненим, коли збирання буде виконано, більшість реалізацій GC мають загальний шаблон виконання збирання під час розподілу. Якщо виділення не зроблено, більшість GS залишаються неактивними.
Розглянемо наступний сценарій:

  1. Зроблено послідовний набір розподілів
  2. Більшість (або всі) з цих елементів позначено як недоступні (уявіть, що посилання обнуляється, що вказує на кеш, який більше не потрібен)
  3. Більше припусків не робиться

У цьому сценарії більшість ГК більше не збиратимуться. Тобто, навіть якщо є недоступні предмети, доступні для збору, вони не витребовуються колектором. Це не витоки самі по собі, але результат вищий, ніж звичайне використання пам’яті.
Google надає чудовий приклад такої поведінки у своїх документах із профілювання пам’яті JavaScript, приклад №2.

Представляємо інструменти для профілювання пам’яті Chrome

Chrome надає хороший набір інструментів для діагностики використання пам’яті кодом JavaScript. Існують переважно два режими перегляду, пов’язані з пам’яттю: перегляд часової шкали та перегляд профілю.

Перегляд часової шкали

Перегляд часової шкали необхідний для виявлення незвичайних шаблонів пам’яті в нашому коді. Коли ми шукаємо великий витік, періодичні стрибки, які не зменшуються настільки, наскільки вони виросли після збору, повинні насторожити вас. На цьому знімку ми бачимо, як виглядає постійне зростання негерметичного об’єкта. Навіть після останньої великої колекції загальний обсяг використаної пам’яті в кінці більше, ніж на початку. Кількість вузлів також. Так багато натяків на витоки DOM десь у коді.

Перегляд профілю

Саме на цей вид ви витратите багато часу, дивлячись на нього. Перегляд профілів дозволяє отримати знімок та порівняти знімки використання пам’яті вашого коду JavaScript.
Це дозволяє зберігати виділення з часом. У всіх результатах перегляду є різні типи списків, але найбільш релевантними для нашого завдання є підсумковий список і список порівняння.
Зведений вигляд дає нам огляд різних типів виділених об’єктів та їх агрегованого розміру: невеликий розмір (сума всіх об’єктів певного типу) і збережений розмір (мілкий розмір плюс розмір інших об’єктів, які зберігаються завдяки цьому об’єкту ). Це також дає поняття про відстань цього об’єкта від його кореня GC (відстань).
Список порівняння дає нам ту саму інформацію, але дозволяє порівнювати різні знімки. Це особливо корисно для пошуку витоків.
Приклад: пошук витоків за допомогою 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. Він також виділить великий масив і додасть до нього масив, на який посилається глобальна змінна. Це призведе до постійного збільшення пам’яті, яке можна знайти за допомогою інструментів, згаданих вище.
Зазвичай ми спостерігаємо коливання використання пам’яті в мовах, заснованих на збиранні сміття. Це те, що очікується, якщо код виконується в циклі, створюючи виділення, що зазвичай буває. Ми будемо шукати періодичне підвищення пам’яті, яке не повертатиметься до попередніх рівнів після збору.
Спочатку подивіться, чи збільшується пам’ять періодично.
Часова шкала View чудово підходить для цього. Відкрийте приклад у Chrome, відкрийте інструменти розробника, перейдіть до шкали часу, виберіть пам’ять і натисніть кнопку запису. Потім перейдіть на сторінку та натисніть кнопку, щоб почати витік пам’яті. Зачекайте трохи, потім припиніть запис і подивіться на результат:

Витік пам’яті у перегляді шкали часу

Цей приклад продовжуватиме витікати пам’яті щосекунди. Після зупинки запису додайте точку зупинки до функції зростання, щоб сценарій не змушував Chrome закривати сторінку. На цьому зображенні є 2 великі підказки, які показують, що у нас витік пам’яті. Графіки вузлів (зелена лінія) і JS-кучки (синя лінія). Вузли постійно збільшуються і ніколи не спадають. Це великий червоний прапор.
JS Heap також демонструє постійне збільшення використання пам'яті. Це важче побачити через дію «Збірника сміття». Ви можете спостерігати модель початкового зростання пам’яті, за яким слідує велике скорочення, потім зростання, потім сплеск, за яким слідує ще одне скорочення. Ключовим моментом у цьому випадку є те, що після кожного зменшення використання пам’яті розмір купи залишається більшим, ніж у попередній раз. Це означає, що, хоча збірник сміття успішно відновлює багато пам’яті, частина регулярно втрачається.
Зараз ми впевнені, що є витік. Давайте знайдемо.
d

Зробіть два знімки

Щоб знайти витік, ми зараз перейдемо до розділу профілю Chrome Dev Tools. щоб зберегти використання пам’яті на керованому рівні, перезавантажте сторінку перед цим кроком. Ми збираємося використовувати функцію «Зробити знімок кучи».
Перезавантажте сторінку та зробіть знімок кучи відразу після його завантаження. Ми будемо використовувати цей знімок як довідник. Тепер знову натисніть кнопку, зачекайте кілька секунд і зробіть новий знімок. Після створення знімка бажано поставити точку зупинки в сценарії, щоб запобігти витоку, що споживає більше пам’яті.

Знімки купи

Існує два способи подивитися на розподіл між двома знімками. Або ви клацніть Підсумок, а потім перейдіть праворуч і виберіть Об’єкти, розміщені між знімком 1 і знімком 2, або натисніть Порівняння замість Резюме. В обох випадках ми побачимо список об’єктів, які були розміщені між двома знімками.
У цьому випадку виявити течі досить просто: вони великі. Подивіться на дельту розміру конструктора (рядка). 8 МБ для 58 нових елементів. Це виглядає підозріло: нові об’єкти виділяються, але не звільняються, і витрачається 8 МБ.
Якщо ми відкриємо список виділень для конструктора (рядка), ми помітимо, що серед безлічі малих є кілька великих виділень. Великі одразу привертають нашу увагу. Якщо ми виберемо будь-який з них, ми знайдемо щось цікаве з ним у розділі Retainers трохи нижче.

Фіксатори для обраного об'єкта

Ми бачимо, що вибраний розподіл є частиною масиву. У свою чергу, на масив посилається змінна x у глобальному об’єкті вікна. Це дає нам повний шлях від нашого великого об’єкта до його кореня (вікна), який не можна колекціонувати. Ми знайшли наш потенційний витік і де на нього посилаються.
Все йде нормально. Але наш приклад був простим: великі виділення, як у прикладі, не є нормою. На щастя, у нашому прикладі також витікають вузли DOM, які менші. Ці вузли легко знайти за допомогою знімка вище, але на великих сайтах все стає складніше. Останні версії Chrome забезпечують додатковий інструмент, який добре підходить для нашої роботи: функцію розподілу пам’яті записів.

Збережіть розподіл купи, щоб знайти витоки

Вимкніть точку зупинки, яку ви встановили раніше, запустіть сценарій і поверніться до розділу «Профіль» інструментів Chrome Dev. Тепер торкніться «Запис розподілу кучі». Під час роботи інструмента ви помітите сині шипи у верхній частині графіка. він представляє надбавки. Кожну секунду код виробляє великий розподіл. Дайте йому працювати кілька секунд, а потім зупиніть його (не забудьте додати точку зупинки, щоб Chrome не споживав більше пам’яті).
Купа збережених надбавок.
На цьому зображенні ви можете побачити вбивчу функцію цього інструменту: виберіть частину часової шкали, щоб побачити, які розподіли були зроблені за цей час. Визначаємо виділення, найближче до великих піків. У списку відображаються лише три конструктори. Один з них пов’язаний з нашим великим витоком ((рядок)), наступний пов’язаний з виділеннями DOM, а останній – конструктором Text (конструктор для листових вузлів DOM, що містять текст).
Виберіть один із конструкторів HTMLDivElement зі списку, а потім натисніть «Вибрати стек розподілу».
Вибраний елемент у результатах розподілу купи
БАМ! Тепер ми знаємо, де був розміщений цей елемент (grow -> createSomeNodes). Якщо ми уважно подивимося на кожну вершину на графіку, то помітимо, що конструктор HTMLDivElement називається багато. Якщо ми повернемося до перегляду порівняння знімків, ми помітимо, що цей конструктор показує багато розподілу, але не видаляє. Іншими словами, він регулярно виділяє пам’ять, не дозволяючи GC її вилучити. Це все симптоми витоку, і, крім того, ми точно знаємо, де ці об’єкти розміщені (функція createSomeNodes). Тепер настав час повернутися до коду, вивчити його та усунути витоки.

  • Ще одна корисна функція:

У перегляді результатів розподілу купи ми можемо вибрати Подання розподілу замість Підсумок.
Цей перегляд дає нам список функцій і пов’язаних розподілів пам’яті. Ми відразу бачимо, що рости та createSomeNodes виділяються. Коли ми вибираємо grow, ми дивимося на конструктори пов’язаних об’єктів, які він викликає. Ми помічаємо (рядок), HTMLDivElement і Text, які, як ми вже знаємо, є конструкторами об’єктів, що витікають.
Поєднання цих інструментів може допомогти знайти витоки. Пограйте з цим, запустіть різне профілювання на своїх виробничих сайтах (в ідеалі — код, не згорнутий або затуманений). Подивіться, чи можете ви знайти якісь витоки або предмети, які зберігаються довше, ніж слід (підказка: їх важче знайти).
Щоб скористатися цими функціями, перейдіть до Інструменти розробника -> Налаштування та увімкніть «запис стека розподілу купи». Це необхідно зробити перед записом.

  • Щоб піти далі:

Управління пам’яттю – мережа розробників Mozilla
Витоки пам’яті JScript – Дуглас Крокфорд (старий, у зв’язку з витоками Internet Explorer 6)
Профілювання пам’яті JavaScript – Документи розробника Chrome
Діагностика пам’яті – Google Developers
Цікавий вид витоку пам’яті JavaScript – блог Meteor
Закриття Grokking V8
Висновок
Витік пам’яті може відбуватися і трапляється в мовах зі збором сміття, як-от JavaScript. Вони можуть деякий час ховатися і в кінцевому підсумку спричинити хаос. З цієї причини інструменти профілювання пам’яті необхідні для виявлення витоків пам’яті. Профілювання має бути частиною циклів розробки, особливо для середніх і великих додатків. Почніть зараз, щоб надати користувачам найкращий досвід.
Гарне налагодження!
Знайдіть першу статтю на цю тему ICI !
[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″]