1. Знайомство з пам'яттю в JVM

Як ти вже знаєш, JVM запускає Java-програми всередині себе. Як і будь-яка віртуальна машина, вона має власну систему організації пам'яті.

Схема організації внутрішньої пам'яті вказує на принцип роботи твоєї Java-програми. Таким чином можна визначити вузькі місця у роботі застосунків та алгоритмів. Давай розберемося, як вона влаштована.

Важливо! Модель Java в початковому вигляді була недостатньо хороша, тому її переглянули у Java 1.5. Ця версія використовується досі (Java 14+).

2. Стек потоку

Модель пам'яті Java, що використовується всередині JVM, поділяє пам'ять на стеки потоків (thread stacks) і купу (heap). Подивимося на Java-модель пам'яті, логічно розділену на блоки:

Стек потоку

Усі потоки, що працюють у JVM, мають свій стек. Стек у свою чергу містить інформацію про те, які методи викликав потік. Я називатиму це “стеком викликів”. Стек викликів відновлюється, щойно потік виконає свій код.

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

Усі локальні змінні примітивних типів (boolean, byte, short, char, int, long, float, double) повністю зберігаються в стеку потоків і не є видимими для інших потоків. Один потік може передати копію примітивної змінної іншому потоку, але не може спільно використовувати примітивну локальну змінну.

3. Купа (heap)

Купа містить усі об'єкти, які створено у вашій програмі, незалежно від того, який потік створив об'єкт. До цього належать і обгортки примітивних типів (наприклад, Byte, Integer, Long тощо). Не має значення, чи об'єкт було створено і присвоєно локальній змінній чи створено як змінну-член іншого об'єкта – він зберігається у купі.

Нижче – діаграма, яка ілюструє стек викликів та локальні змінні (вони зберігаються у стеках), а також об'єкти (вони зберігаються у купі):

Купа (heap)

Якщо локальна змінна – примітивного типу, вона зберігається в стеку потоку.

Локальна змінна може бути посиланням на об'єкт. І тут посилання (локальна змінна) зберігається у стеку потоків, але сам об'єкт зберігається у купі.

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

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

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

4. Взаємодія з об'єктами

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

Взаємодія з об'єктами (heap)

Два потоки мають набір локальних змінних. Local Variable 2 вказує на загальний об'єкт у купі (Object 3). Кожен із потоків має свою копію локальної змінної зі своїм посиланням. Їхні посилання є локальними змінними, і тому вони зберігаються в стеках потоків. Тим не менш, два різні посилання вказують на той самий об'єкт у купі.

Зверни увагу, що загальний Object 3 має посилання на Object 2 і Object 4 як змінні-члени (показано стрілками). Через ці посилання два потоки можуть отримати доступ до Object 2 і Object 4.

На діаграмі також показано локальну змінну (Local variable 1 з methodTwo). Кожна її копія містить різні посилання, які вказують на два різні об'єкти (Object 1 і Object 5), а не на один і той самий. Теоретично обидва потоки можуть звертатися як до Object 1, так і до Object 5, якщо вони мають посилання на обидва об'єкти. Але на діаграмі вище кожен потік має посилання лише на один із двох об'єктів.

5. Приклад взаємодії з об'єктами

Давай подивимося, як ми можемо продемонструвати роботу в коді:


 public class MySomeRunnable implements Runnable() {

    public void run() {
        one();
    }

    public void one() {
        int localOne = 1;

        Shared localTwo = Shared.instance;

        //… щось робимо з локальними змінними

        two();
    }

    public void two() {
        Integer localOne = 2;

        //… щось робимо з локальними змінними
    }
}

public class Shared {

    // зберігаємо інстанс на наш объект у змінній

    public static final Shared instance = new Shared();

    // змінні-члени, які вказуть на два об'єкти в купі

    public Integer object2 = new Integer(22);
    public Integer object4 = new Integer(44);
}

Метод run() викликає метод one(), а one() – викликає two().

Метод one() оголошує примітивну локальну змінну (localOne) типу int і локальну змінну (localTwo), яка є посиланням на об'єкт.

Кожен потік, що виконує метод one(), створить власну копію localOne і localTwo у своєму стеку. Змінні localOne будуть повністю відокремлені одна від одної під час перебування у стеку кожного потоку. Один потік не може бачити, які зміни вносить інший потік у свою копію localOne.

Кожен потік, що виконує метод one(), також створює власну копію localTwo. Однак дві різні копії localTwo зрештою вказують на той самий об'єкт у купі. Справа в тому, що localTwo вказує на об'єкт, на який посилається статична змінна instance. Існує лише одна копія статичної змінної, і ця копія зберігається у купі.

Таким чином, обидві копії localTwo зрештою вказують на той самий екземпляр Shared. Примірник Shared також зберігається у купі. Він відповідає Object 3 на діаграмі вище.

Зверни увагу, що клас Shared також містить дві змінні-члени. Самі змінні-члени зберігаються у купі разом із об'єктом. Дві змінні-члени вказують на два інші об'єкти Integer. Ці цілі численні об'єкти відповідаютьObject 2 і Object 4 на діаграмі.

Метод two() створює локальну змінну з ім'ям localOne. Ця локальна змінна є посиланням на об'єкт типу Integer. Метод встановлює посилання localOne для зазначення нового екземпляру Integer. Посилання зберігатиметься у своїй копії localOne для кожного потоку. Два екземпляри Integer збережуться в купі і, оскільки метод створює новий об'єкт Integer при кожному виконанні, два потоки, що виконують цей метод, будуть створювати окремі екземпляри Integer. Вони відповідають Object 1 і Object 5 на діаграмі вище.

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