Керування пам'яттю

У низькорівневих мовах, наприклад C, існують примітивні функції для ручного керування пам'яттю, такі як malloc() та free(). В той же час JavaScript автоматично виділяє пам'ять при створенні об'єктів та звільняє її, коли вони більше не використовуються (збирання сміття). Ця автоматичність є потенційним джерелом плутанини: вона може дати розробникам хибне враження, що їм не потрібно хвилюватись щодо керування пам'яттю.

Життєвий цикл пам'яті

Незалежно від мови програмування, життєвий цикл пам'яті завжди приблизно той самий:

  1. Виділення потрібної пам'яті
  2. Використання виділеної пам'яті (читання, запис)
  3. Вивільнення виділеної пам'яті, коли вона більше не потрібна

Друга частина в усіх мовах є явною. Перша та остання частини є явними у низькорівневих мовах, але, як правило, неявні у мовах високого рівня, таких як JavaScript.

Виділення пам'яті у JavaScript

Ініціалізація значень

Щоб не турбувати розробника виділеннями пам'яті, JavaScript автоматично виділяє пам'ять, коли оголошуються початкові значення.

var n = 123; // виділяє пам'ять для числа
var s = 'блабла'; // виділяє пам'ять для рядка

var o = {
  a: 1,
  b: null
}; // виділяє пам'ять для об'єкта та значень, що він містить

// (як з об'єктом) виділяє пам'ять для масиву та
// значень, що він містить
var a = [1, null, 'аовл']; 

function f(a) {
  return a + 2;
} // виділяє пам'ять для функції (об'єкт, який можна викликати)

// функціональні вирази також виділяють пам'ять для об'єкта
someElement.addEventListener('click', function() {
  someElement.style.backgroundColor = 'blue';
}, false);

Виділення пам'яті через виклики функцій

Виклики деяких функцій виділяють пам'ять під об'єкт.

var d = new Date(); // виділяє пам'ять під об'єкт Date

var e = document.createElement('div'); // видділяє пам'ять під елемент DOM

Деякі методи виділяють пам'ять для нових значень чи об'єктів:

var s = 'йцуке';
var s2 = s.substr(0, 3); // s2 - новий рядок
// Оскільки рядки є незмінними, 
// JavaScript може вирішити не виділяти пам'ять, 
// а лише зберегти діапазон [0, 3].

var a = ['ouais ouais', 'nan nan'];
var a2 = ['generation', 'nan nan'];
var a3 = a.concat(a2); 
// новий масив з 4 елементів, що є
// поєданням елементів a та a2.

Використання значень

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

Звільнення пам'яті, коли вона більше не потрібна

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

Низькорівневі мови вимагають, щоб розробник вручну визначав, у якій точці програми виділена пам'ять більше не потрібна, та звільняв її.

Деякі високорівневі мови, такі як JavaScript, використовують форму автоматичного керування пам'яттю, відому як збирання сміття (ЗС). Збирач сміття відслідковує виділення пам'яті та визначає момент, коли блок виділеної пам'яті більше не потрібний, і повертає його. Цей автоматичний процес є неточним, оскільки загальна проблема визначення, чи потрібна наразі та чи інша ділянка пам'яті, є нерозв'язною.

Збирання сміття

Як було зазначено вище, загальна проблема автоматичного визначення того, що якась ділянка пам'яті "більше не потрібна", є нерозв'язною. Як наслідок, збирачі сміття впроваджують обмеження вирішення загальної проблеми. Цей розділ пояснює концепції,  необхідні для розуміння головних алгоритмів збирання сміття та відповідних обмежень.

Посилання

Головна концепція, на яку покладаються алгоритми збирачів сміття - це концепція посилань. У контексті керування пам'яттю об'єкт посилається на інший об'єкт, якщо звертається до нього (явно чи неявно). Наприклад, об'єкт JavaScript має посилання на свій прототип (неявне посилання) та на значення своїх властивостей (явне посилання).

У цьому контексті поняття "об'єкта" є ширшим, ніж звичайні об'єкти JavaScript, і містить також області видимості функцій (або глобальну область видимості).

Збирання сміття через підрахунок посилань

Це найпримітивніший алгоритм збирання сміття. Цей алгоритм звужує проблему з визначення того, чи об'єкт досі є потрібним, до визначення того, чи мають інші об'єкти досі посилання на даний об'єкт. Об'єкт називається "сміттям", або підлягає збиранню, якщо на нього вказує нуль посилань.

Приклад

var x = { 
  a: {
    b: 2
  }
}; 
// Створено 2 об'єкта. Один має посилання у іншому як одна з його властивостей.
// Інший має посилання через присвоєння його змінній 'x'.
// Зрозуміло, що жоден з них не може бути прибраний.


var y = x;      // Змінна 'y' - друга сутність, що має посилання на об'єкт.

x = 1;          // Тепер об'єкт, що початково був присвоєний 'x', має унікальне посилання,
                // втілене у змінній 'y'.

var z = y.a;    // Посилання на властивість 'a' об'єкта.
                //   Цей об'єкт тепер має 2 посилання: одне у вигляді властивості, 
                //   інше у вигляді змінної 'z'.

y = 'mozilla';  // Об'єкт, що початково був присвоєний 'x', тепер має нуль посилань.
                //   Він може бути прибраний.
                //   Однак, на його властивість 'a' досі посилається 
                //   змінна 'z', тому її не можна вивільнити.

z = null;       // Властивість 'a' об'єкта, що початково був присвоєний x,
                //   має нуль посилань. Об'єкт тепер може бути прибраний.

Обмеження: Циклічні посилання

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

function f() {
  var x = {};
  var y = {};
  x.a = y;        // x посилається на y
  y.a = x;        // y посилається на x

  return 'йцуке';
}

f();

Приклад з реального життя

Internet Explorer 6 та 7 відомі тим, що їхні збирачі сміття для об'єктів DOM працюють на основі підрахунку посилань. Цикли є типовою помилкою, що призводить до витоків пам'яті:

var div;
window.onload = function() {
  div = document.getElementById('myDivElement');
  div.circularReference = div;
  div.lotsOfData = new Array(10000).join('*');
};

У наведеному прикладі DOM-елемент "myDivElement" має циклічне посилання на самого себе у властивості "circularReference". Якщо ця властивість не буде явно видалена чи обнулена, збирач сміття, що підраховує посилання, завжди матиме принаймні одне наявне посилання і триматиме DOM-елемент у пам'яті, навіть якщо він був видалений з DOM-дерева. Якщо DOM-елемент містить великий об'єм даних (проілюстровано у наведеному прикладі властивістю "lotsOfData"), пам'ять, зайнята цими даними, ніколи не буде вивільнена, що може призвести до проблем, пов'язаних з пам'яттю, наприклад, переглядач ставатиме все повільнішим.

Алгоритм маркування та прибирання (mark-and-sweep)

Цей алгоритм звужує визначення "об'єкта, який більше не потрібен" до "недосяжного об'єкта".

Даний алгоритм використовує набір об'єктів, що називаються коренями. У JavaScript корінь є глобальним об'єктом. Періодично збирач сміття починатиме від цих коренів, знаходитиме усі об'єкти, що мають посилання у коренях, далі усі об'єкти, що мають посилання у них, і т. д. Таким чином, починаючи від коренів, збирач сміття знайде усі досяжні об'єкти та збере усі недосяжні об'єкти.

Цей алгоритм є покращенням попереднього, оскільки об'єкт, що має нуль посилань, є об'єктивно недосяжним. Протилежне не є істиною, як ми бачили на прикладі циклічних посилань.

Станом на 2012 рік усі сучасні переглядачі використовують збирачі сміття з алгоритмом маркування та прибирання. Усі покращення в галузі збирання сміття у JavaScript (генераційне/інкрементне/конкурентне/паралельне збирання сміття) за останні кілька років є покращеннями реалізації даного алгоритму, а не покращеннями самого алгоритму збирання сміття чи скороченням його визначення моменту, коли "об'єкт більше не потрібен".

Циклічні посилання більше не є проблемою

У наведеному вище прикладі, коли завершується виклик функції, два об'єкти більше не мають посилань з жодного ресурсу, досяжного з глобального об'єкта. Як наслідок, збирач сміття визнає їх недосяжними та поверне виділену під них пам'ять.

Обмеження: Ручне вивільнення пам'яті

Бувають випадки, коли було б зручно власноруч визначити, коли й яка пам'ять буде звільнена. Щоб звільнити пам'ять об'єкта, його треба зробити явно недосяжним.

Станом на 2019 рік немає можливості явно чи програмно запустити збирання сміття у JavaScript.

Node.js

Node.js пропонує додаткові можливості та інструменти для конфігурування та відлагодження проблем з пам'яттю, які можуть бути недоступні для JavaScript, що виконується у середовищі переглядача.

Прапори рушія V8

Максимальний розмір доступної пам'яті купи може бути збільшений за допомогою прапору:

node --max-old-space-size=6000 index.js

Ми також можемо викликати збирач сміття для відлагодження проблем з пам'яттю, використовуючи прапор та Chrome Debugger:

node --expose-gc --inspect index.js

Див. також