Оптимізація і розуміння управління пам'яттю в JavaScript

Оптимізація і розуміння управління пам'яттю в JavaScript

  • 23 лютого
  • читати 10 хв
Володимир Шайтан
Володимир Шайтан Lead Front-end Developer у One Source, Викладач Комп'ютерної школи Hillel.

Управління пам'яттю в JavaScript виконується автоматично і непомітно для нас. Ми створюємо примітиви, об’єкти, функції… Усе це потребує пам’яті.

Що відбувається, коли щось більше не потрібно? Як механізм JavaScript виявляє це та очищає?

У цій статті дізнаємося, що таке Garbage collection (GC), навіщо він потрібен узагалі і які проблеми вирішує.

Для розуміння: перша мова, яка підтримувала збирання сміття - це була мова APL у 1964 році. Ви тільки уявіть, що навіть 60 років тому люди вже думали про те, що потрібно спробувати звільнити час розробників для більш корисних задач!

Тож, GC (Garbage Collection - збирання сміття) - високорівнева абстракція, яка позбавляє розробників необхідності піклуватися про звільнення керованої пам'яті.

Зараз JS, як і багато інших мов програмування, мають здатність активно модифікувати типи даних, тому рушіям необхідно знаходити рішення для роботи з неконтрольованим використанням пам'яті.
Основною концепцією керування пам’яттю в JavaScript є доступність. Збирач сміття стежить за всіма об'єктами і видаляє ті, які стали недоступними.

Простіше кажучи, «досяжні» значення — це ті, які доступні або які можна використовувати. Вони гарантовано зберігаються в пам'яті.
Які це значення:

  • Функція, що виконується в даний момент, її локальні змінні та параметри.

  • Інші функції в поточному ланцюжку вкладених викликів, їх локальні змінні та параметри.

  • Глобальні змінні.

Рекомендуємо курс по темі

Як відбувається процес збирання сміття?

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

Регулярно виконуються наступні кроки:

  • Збирач сміття бере <root>ʼs і «позначає» (тобто запам'ятовує) їх.

  • Потім він проходиться по ним та «позначає» всі посилання, які є всередині.

  • Потім він проходиться по вже позначеним об’єктиам та позначає їх посилання. Всі відвідані об'єкти запам'ятовуються, щоб не відвідувати один і той же об'єкт двічі в майбутньому.

  • …І так далі, поки не буде відвідано кожне доступне (від коренів) посилання.

  • Всі об'єкти, крім позначених, видаляються.

Приклад для пояснення

Структура обʼєкту виглядає так:

З правого боку видно обʼєкти, до яких нічого не веде. Ще вони можуть називатися “недосяжним островом”.

Перший крок - позначає <root>’s:

Потім ми слідуємо за їхніми посиланнями та позначаємо об’єкти, на які вони посилаються:

Продовжуємо стежити за подальшими посиланнями, поки це можливо:

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

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

Покоління:

  1. молоде (young)

  2. середнє (middle)

  3. старе (old)

Нові обʼєкти зазвичай створюються в молодому поколінні. Середнім поколінням вони стають після певної кількості зайнятої памʼяті або певної кількості ітерацій. Якщо обʼєкти “виживають”, то вони переходять до наступного покоління. Грубо кажучи, збірка сміття відбувається між кожним таким “переходом”. Старі покоління, в свою чергу, рідко піддаються перевіркам збірником сміття, тому що обʼєкти всередині них прийнято вважати більш стійкими.

Як ми можемо оптимізувати роботу з памʼяттю, допомагаючи тим самим GC?

За таке довге існування проблем з памʼяттю розробники виділили декілька основних стратегій оптимізації коду:

1. Намагаємося уникати витоку памʼяті:

  • Використовуємо замикання та інші механізми для управління областями видимості та памʼяттю

2. Уникаємо глибокої вкладеності:

  • Це може призвести до ще більшого обʼєму памʼяті, який потрібно буде збирати

3. Ефективне використання памʼяті:

  • Якщо це можливо, то краще уникати великих обʼєктів і масивів, розділяючи їх на модулі та окремі масиви.

4. Оптимізація циклів попереджує надміну рекурсію

  • Надмірна рекурсія може призвести до створення багато непотрібних об'єктів в пам'яті.

5. Використовуйте вбудовані методи для оптимізації роботи з пам'яттю:

  • JavaScript і ще деякі мови програмування надають вбудовані методи для оптимізації роботи з пам'яттю, такі як методи для керування пам'яттю типу "WeakMap" або "WeakSet".

6. Мінімізувати створення непотрібних об'єктів:

  • Замість того, щоб створювати нові об'єкти кожного разу, коли це можливо, перевикористовуйте існуючі.

Висновок

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

Щоб покращити продуктивність коду та оптимізувати використання ресурсів, важливо досліджувати глибокі процеси в мовах програмування та вивчати кращі практики програмування. Дізнаватися про різні алгоритми, шаблони проектування та оптимізаційні техніки допоможе писати чистий, ефективний і оптимізований код.

Ці рекомендації дозволять розробникам краще розуміти принципи роботи мови і використовувати її можливості максимально ефективно.

Пишіть, перевіряйте, навчайтеся і обовʼязково насолоджуйтеся цим процесом.

Рекомендуємо курс по темі