Прокачайся в код-рев’ю: для перших 50 учасників — курс безкоштовний

час читання: 5 хв

Що таке збирання сміття в Java і як воно працює?

У статті описується процес збирання сміття в Java, його суть та еволюція, а також наводяться приклади того, як це влаштовано.

Автор статті — EPAM Senior Software Engineer Вайбхаві Дешпанде.

Читати інші статті експерта:

Мета збирання сміття в Java

Мета збирання сміття в Java — автоматичне керування пам’яттю шляхом її звільнення у випадку, коли вона зайнята об’єктами, що не використовуються. Це допомагає запобігти витокам пам’яті та знижує навантаження на ручне керування нею. Збирання сміття передбачає механізм для визначення та очищення непотрібних об’єктів і звільнення пам’яті для розміщення нових об’єктів.

Еволюція збирання сміття

Перед тим, як збирання сміття з’явилося в Java, мови програмування, такі як C та C++, потребували ручного керування пам’яттю. Розробникам доводилося спеціально виділяти та звільняти пам’ять для об’єктів, використовуючи функції malloc() та free(). Ручне керування пам’яттю часто призводило до проблем: витоків пам’яті, висячих покажчиків та помилок сегментації. Це був складний процес із високою ймовірністю помилок, який вимагав ретельного відстеження процесів виділення та звільнення пам’яті — алокації та деалокації.

Одним з основних досягнень Java стало створення автоматичного збирання сміття. У віртуальній машині Java (JVM) є збирач сміття, який автоматично керує пам’яттю від імені розробника. Збирач сміття визначає та звільняє пам’ять, зайняту об’єктами, які більше не доступні або не використовуються додатком. Таке автоматичне керування пам’яттю спрощує процес розробки, знижує ймовірність багів, пов’язаних із пам’яттю, і підвищує загальну стабільність додатка.

Еволюція збирання сміття в Java призвела до вдосконалення її алгоритмів, методів оптимізації та можливостей налаштування відповідно до вимог різних додатків. Згодом у JVM було впроваджено кілька збирачів сміття з різними характеристиками і компромісами, що дають змогу розробникам вибрати найбільш підходящий збирач залежно від потреб додатка.

Спочатку JVM використовувала простий збирач сміття, що працює за алгоритмом позначок (англ. mark-sweep), який позначав досяжні об’єкти і прибирав недосяжні. Однак за такого підходу виникали проблеми фрагментації пам’яті. Згодом у JVM було введено алгоритм компактизації (англ. mark-compact) з фазою стиснення, щоб знизити фрагментацію пам’яті.

Java-додатки поступово ставали складнішими, тому виникла потреба в більш ефективних механізмах збирання сміття. У JVM було представлено збирання сміття за поколіннями, яке, залежно від віку об’єкта, розділяє купу (англ. Java heap) — одну з областей пам’яті — на молоде і старе покоління. Цей спосіб оптимізує збирання сміття шляхом частішого і швидшого збирання в молодому поколінні та менш частого — у старому.

Далі в JVM впровадили збирачі сміття, орієнтовані на різні сценарії застосування. У сучасних JVM є наступні збирачі сміття: Serial Collector, Parallel Collector, Concurrent Mark Sweep (CMS) Collector і Garbage-First (G1) Collector. Вони пропонують різні компроміси між пропускною здатністю, латентністю і використанням пам’яті, даючи змогу розробникам вибрати найбільш підходящий збирач для свого додатка.

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

10 цікавих фактів про збирання сміття в Java

1. HotSpot JVM

HotSpot JVM, розроблена Sun Microsystems (зараз належить Oracle), — одна з найбільш широко використовуваних віртуальних машин для виконання Java-додатків. У ній передбачено різні алгоритми збирання сміття і збирачі для оптимізації управління пам’яттю.

2. Stop-the-world паузи

Під час збирання сміття JVM зазвичай призупиняє потоки виконання додатка. Ці так звані stop-the-world паузи тимчасово зупиняють процеси в додатку. Однак сучасні збирачі сміття прагнуть мінімізувати тривалість і частоту таких пауз, щоб уникнути значних збоїв.

3. Паралельне збирання сміття

Concurrent Mark Sweep (CMS) та Garbage-First (G1) - приклади збирачів сміття, які здійснюють збирання паралельно з виконанням потоків додатка. Вони прагнуть мінімізувати stop-the-world паузи, виконуючи в додатку операції зі збирання сміття одночасно з кодом.

4. Фрагментація пам’яті

Фрагментація пам’яті може виникати в купі, коли вільна пам’ять розділена на невеликі несуміжні блоки. Це може призвести до неефективного використання пам’яті та збільшення навантаження на її виділення. Збирачі сміття, що виконують стиснення, наприклад, алгоритм mark-compact, допомагають знизити фрагментацію. Це відбувається шляхом перерозподілу пам’яті для створення суміжних блоків вільної пам’яті.

5. Збирання сміття за поколіннями

Збирання сміття за поколіннями засноване на спостереженні, що більшість об’єктів стають сміттям незабаром після створення. Розділяючи купу на молоде і старе покоління, JVM оптимізує збирання сміття, частіше і швидше збираючи молоді об’єкти, а старі — рідше.

6. Налаштування та кастомізація

Збирачі сміття в JVM часто передбачають різні опції налаштування та параметри для кастомізації їхньої поведінки. Розробники можуть налаштовувати ці параметри, щоб оптимізувати продуктивність збирання сміття відповідно до конкретних характеристик і вимог додатка.

7. Алгоритми збирання сміття

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

8. Вплив на продуктивність додатка

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

9. Витоки пам’яті

Хоча збирання сміття допомагає запобігти багатьом витокам пам’яті, воно не виключає можливості їх виникнення. Витоки пам’яті можуть відбуватися, якщо об’єкти випадково залишаються доступними за strong references, або якщо ресурси не ввільняються належним чином. Щоб цього уникнути, необхідно застосовувати відповідні методи програмування та керування пам’яттю.

10. Постійні дослідження та поліпшення

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

Збирання сміття в Java — це складний і цікавий аспект керування пам’яттю. Розуміння її принципів та оптимізацій може значно допомогти вам створювати ефективні та надійні Java-додатки.

Як працює збирання сміття в Java

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

  • Досяжність об’єктів

Збирач сміття визначає, які об’єкти все ще використовуються, а які підходять для збирання. Він починає розглядати набір кореневих об’єктів — статичні змінні, стек виклику і локальні змінні. Будь-який об’єкт, на який безпосередньо або побічно посилаються ці кореневі об’єкти, вважається досяжним і не підлягає збиранню сміття. Об’єкти, які не доступні з кореневих об’єктів, вважаються недосяжними і можуть бути зібрані. Для визначення досяжності об’єктів у Java використовується структура на основі теорії графів.

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

  • Алгоритм позначок

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

Приклад:

Як створити кілька екземплярів класу MyClass

У наведеному вище коді ми створюємо кілька екземплярів класу MyClass. Ми також створюємо посилання obj4 та obj5, які вказують на obj1 та obj2 відповідно. Потім ми присвоюємо obj1,obj2 та obj3 значення null, щоб вони стали доступними для збирання сміття. Нарешті, ми безпосередньо викликаємо метод System.gc() для запиту збирання сміття.

Коли збирач сміття працює, він позначає всі досяжні об’єкти, починаючи з кореневих (obj4 таobj5). Оскільки obj1,obj2 та obj3 більше не є досяжними, вони будуть визначені як недосяжні на фазі очищення. У результаті їхню пам’ять буде відновлено, і для кожного з цих об’єктів буде викликано метод finalize().

  • Фіналізація

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

Приклад: Припустимо, у вас є об’єкт підключення до бази даних, який потрібно правильно закрити перед збиранням сміття для звільнення всіх накопичених ресурсів. У метод finalize() можна вставити код, щоб завершити з’єднання з базою даних.

Як закрити підключення до бази даних

У наведеному вище коді метод finalize() класу DatabaseConnection перевизначається для виклику методу close(). Це потрібно, щоб правильно закрити підключення до бази даних, перед тим як збирач сміття видалить цей об’єкт.

Висновки

Таким чином, збирання сміття в Java включає в себе визначення і відновлення пам’яті, яку займають недосяжними об’єктами. Для визначення цих об’єктів застосовується алгоритм позначок. Для очищення об’єктів перед збиранням сміття може використовуватися метод finalize(). Розуміючи, як працює збирання сміття, Java-розробники можуть писати ефективний і безпечний для пам’яті код.