Вітаю!

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

Яке відношення до всього цього має програмування на Java? Пряме! Адже під час створення будь-якого об’єкта Java-машиною під нього виділяється пам’ять. У реальній великій програмі створюються десятки та сотні тисяч об’єктів, під кожен із яких у пам’яті виділяється свій шматочок.

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

За всіх переваг Java-об’єктів вони не безсмертні :) Об’єкти мають власний життєвий цикл.

Сьогодні ми трохи відпочинемо від написання коду і розглянемо цей процес :) Тим більше, що він є дуже важливим для розуміння роботи програми та розпорядження ресурсами.

Отже, з чого ж починається життя об’єкта? Як і в людини — з його народження, тобто створення.


Cat cat = new Cat();//ось зараз і розпочався життєвий цикл нашого об’єкта Cat!

Спочатку віртуальна Java-машина виділяє необхідний обсяг пам’яті для створення об’єкта.

Потім вона створює на нього посилання, у нашому випадку — cat, щоб мати можливість його відстежувати. Після цього відбувається ініціалізація всіх змінних, виклик конструктора і ось — наш свіжий об’єкт вже живе своїм життям :)

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

Якщо говорити точно, об’єкт є «живим» поки на нього є посилання. Тільки-но посилань не залишається, — об’єкт «вмирає».

Наприклад:


public class Car {
  
   String model;

   public Car(String model) {
       this.model = model;
   }

   public static void main(String[] args) {
       Car lamborghini  = new Car("Lamborghini Diablo");
       lamborghini = null;

   }

}

У методі main() об’єкт машини Lamborghini Diablo перестає бути живим уже на другому рядку. На нього було лише одне посилання, а тепер цьому посиланню було присвоєно null. Оскільки на Lamborghini Diablo не залишилося посилань, він стає «сміттям».

Посилання при цьому не обов’язково обнуляти:


public class Car {

   String model;

   public Car(String model) {
       this.model = model;
   }

   public static void main(String[] args) {
       Car lamborghini  = new Car("Lamborghini Diablo");

       Car lamborghiniGallardo = new Car("Lamborghini Gallardo");
       lamborghini = lamborghiniGallardo;
   }

}

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

І в цей момент в роботу вступає вбудований механізм Java під назвою збирач сміття, або по-іншому — Garbage Collector, GC.

Життєвий цикл об’єкта  - 2
Збирач сміття – внутрішній механізм Java, який відповідає за звільнення пам’яті, тобто видалення з неї непотрібних об’єктів.

Ми не дарма обрали для його зображення картинку з роботом-пилососом. Адже збирач сміття працює приблизно так само: у фоновому режимі він «їздить» по твоїй програмі, збирає сміття, і при цьому ти з ним практично не взаємодієш.

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

Так от, у випадку з Java-об’єктами збирач сміття робить це замість тебе.

Garbage Collector запускається багаторазово протягом роботи твоєї програми: його не треба викликати спеціально та віддавати команди, хоча технічно це можливо.

Пізніше ми ще поговоримо про нього та розберемо процес його роботи детальніше.

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

Його можна використовувати, щоб звільнити якісь додаткові ресурси, які використовував об’єкт.

Метод finalize() належить до класу Object. Тобто нарівні з equals(), hashCode() і toString(), з якими ти вже ознайомився раніше, він є у будь-якого об’єкта.

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

А саме перед знищенням об’єкта він викликається далеко не завжди.

Програмування — штука точна. Програміст каже комп’ютеру щось зробити — комп’ютер це робить. Ти, гадаю, вже звик до такої поведінки, і тобі спочатку може бути складно прийняти ідею: «Перед знищенням об’єктів викликається метод finalize() класу Object. Або не викликається. Як пощастить!»

Проте, це дійсно так. Java-машина сама визначає, чи викликати метод finalize()у кожному конкретному випадку, чи ні.

Наприклад, давай спробуємо заради експерименту запустити такий код:


public class Cat {

   private String name;

   public Cat(String name) {
       this.name = name;
   }

   public Cat() {
   }

   public static void main(String[] args) throws Throwable {
       for (int i = 0 ; i < 1000000; i++) {
           Cat cat = new Cat();
           cat = null;//ось тут перший об’єкт стає доступним збирачу сміття
       }
   }

   @Override
   protected void finalize() throws Throwable {
       System.out.println("Об’єкт Cat знищено!");
   }
}

Ми створюємо об’єкт Cat і вже в наступному рядку коду обнуляємо єдине посилання на нього. І так мільйон разів.

Ми явно перевизначили метод finalize(), і він має мільйон разів вивести рядок у консоль, щоразу перед знищенням об’єкта Cat.

Але ні! Якщо бути точним, на моєму комп’ютері він відпрацював лише 37346 разів! Тобто тільки в 1 випадку з 27-ми встановлена у мене Java-машина приймала рішення викликати метод finalize(), в інших випадках збирання сміття проходило без цього.

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

Як бачиш, finalize() важко назвати надійним партнером :)

Тому невелика порада на майбутнє: не варто покладатися на метод finalize() у випадку зі звільненням якихось критично важливих ресурсів. Може, JVM його викличе, а, може, ні. Хто знає? Якщо твій об’єкт за життя займав якісь суперважливі для продуктивності ресурси, наприклад, тримав відкритим з’єднання з базою даних, краще створи у своєму класі спеціальний метод для їхнього звільнення та виклич його явно, коли об’єкт вже буде не потрібен. Так ти точно знатимеш, що продуктивність твоєї програми не постраждає.

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

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

Оскільки ми навели аналогію з роботом-пилососом, уяви, що буде, якщо перед запуском робота розкидати по дому шкарпетки, розбити скляну вазу і залишити на підлозі розібраний конструктор Lego. Робот, звісно, спробує щось зробити, але одного разу він застрягне.

Життєвий цикл об’єкта  - 3
Для його правильної роботи потрібно тримати підлогу у нормальному стані та прибирати звідти все, з чим не впорається пилосос.

За таким самим принципом працює і збирач сміття. Якщо у програмі буде залишатися багато об’єктів, які він не може зібрати (як носок або Lego для робота-пилососа), одного разу пам’ять закінчиться. І може зависнути не тільки написана тобою програма, а й інші програми, запущені у цей момент на комп’ютері. Для них також може не вистачати пам’яті.

Це не потрібно заучувати: достатньо просто зрозуміти принцип роботи.