JavaRush /Курси /JAVA 25 SELF /Влаштування пам’яті в JVM: стек, купа, PermGen/Metaspace

Влаштування пам’яті в JVM: стек, купа, PermGen/Metaspace

JAVA 25 SELF
Рівень 64 , Лекція 0
Відкрита

1. Огляд пам’яті процесу Java

Коли ви запускаєте Java-програму, JVM (Java Virtual Machine) просить у операційної системи шматочок пам’яті. Іноді — скромний, іноді — доволі значний (особливо якщо ви запускаєте якийсь Minecraft із купою модів). Ця пам’ять ділиться на кілька ключових областей, кожна з яких відіграє свою роль:

  • Стек (Stack) — для локальних змінних і викликів методів.
  • Купа (Heap) — для всіх об’єктів, які ви створюєте через new.
  • Службові області (PermGen/Metaspace) — для метаданих класів та інших «магічних» речей.

Це виглядає приблизно так:

┌───────────────────────────────┐
│ JVM-процес                    │
│ ┌─────────────┐               │
│ │  Stack      │ ← Кожен потік — свій стек!
│ └─────────────┘               │
│ ┌─────────────┐               │
│ │  Heap       │ ← Спільний для всіх потоків
│ └─────────────┘               │
│ ┌───────────────┐             │
│ │ PermGen/      │ ← Метадані класів
│ │ MetaSpace     │
│ └───────────────┘             │
└───────────────────────────────┘

Чому це важливо?

  • Розуміння влаштування пам’яті допомагає писати ефективніший і безпечніший код.
  • Простіше діагностувати помилки типу StackOverflowError або OutOfMemoryError.
  • Не страшні слова «збирач сміття» і «витік пам’яті» — ви розумієте, де й що шукати.

2. Стек (Stack): швидко, локально, але не назавжди

Стек — це спеціальна область пам’яті, виділена для кожного потоку окремо. Стек схожий на стос тарілок: останню поклали — першу заберете тільки після того, як знімете всі інші. Тобто стек працює за принципом LIFO (Last In, First Out).

Навіщо потрібен стек?

У стеку зберігаються:

  • Локальні змінні методів (наприклад, int x = 5; усередині методу).
  • Адреса повернення після виклику методу (щоб знати, куди повернутися після завершення роботи методу).

Кожного разу, коли ви викликаєте метод, у стек додається новий фрейм (stack frame) — щось на кшталт коробки, у якій лежать усі локальні змінні цього методу та службова інформація. Коли метод закінчує роботу, його фрейм видаляється — усі його локальні змінні зникають.

Приклад

public static void main(String[] args) {
    int a = 10;           // a лежить у стеку main
    int b = sum(a, 5);    // викликаємо sum
}

public static int sum(int x, int y) {
    int result = x + y;   // x, y, result лежать у стеку sum
    return result;
}
  • Коли викликається sum, для нього створюється окремий фрейм у стеку.
  • Після завершення роботи sum його змінні зникають.

Життєвий цикл змінної

Локальні змінні живуть лише доти, доки виконується метод, у якому вони оголошені. Щойно метод завершився — їх уже немає, пам’ять звільняється миттєво.

Переповнення стеку

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

Exception in thread "main" java.lang.StackOverflowError

Приклад:

public static void main(String[] args) {
    recurse();
}
public static void recurse() {
    recurse(); // Нескінченна рекурсія!
}

Розмір стеку

Розмір стеку обмежений — зазвичай це кілька мегабайт на потік (можна задати параметром -Xss). Якщо стек закінчився — програма падає з помилкою.

3. Купа (Heap): місце для ваших об’єктів

Купа (Heap) — це спільна область пам’яті для всіх потоків, де живуть усі об’єкти, які ви створюєте за допомогою new, а також масиви. Саме в купі відбувається вся магія об’єктно-орієнтованого програмування.

Як об’єкти потрапляють до купи?

String s = new String("Hello");
int[] arr = new int[10];
  • Змінна s — це посилання, воно лежить у стеку.
  • Сам об’єкт String і масив arr — лежать у купі.

Життєвий цикл об’єкта

Об’єкт живе в купі доти, доки на нього існує хоча б одне сильне посилання (strong reference). Щойно на об’єкт ніхто не посилається — він стає «сміттям» і може бути видалений збирачем сміття (GC).

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

На відміну від C/C++, де ви самі маєте дбати про звільнення пам’яті (free, delete), у Java цим займається GC. Ви не можете явно звільнити об’єкт, але можете присвоїти null усім посиланням на нього — тоді він стане кандидатом на видалення.

Схема: де що лежить?

Stack (main)
  └─ s ─┬────────────┐
         │           │
         ▼           │
      Heap           │
   ┌─────────────┐   │
   │ String "Hello"◄──┘
   └─────────────┘

Особливості купи

  • Купа одна на весь процес JVM.
  • Розмір купи можна задавати під час запуску (-Xmx, -Xms).
  • Якщо в купі не залишилося вільного місця і GC не може звільнити пам’ять — програма падає з помилкою OutOfMemoryError.

4. PermGen і Metaspace: де «живуть» класи?

Коли ви пишете class MyClass { ... }, а потім запускаєте програму, JVM має десь зберігати все, що пов’язано з цим класом — метадані про методи й поля, байт‑код, константи тощо. Для цього в JVM існує особлива область пам’яті, де «живуть» класи.

Раніше, до Java 8, цю область називали PermGen (Permanent Generation). Але в неї було чимало проблем — наприклад, вона мала фіксований розмір, і якщо місця не вистачало, застосунок просто вилітав із помилкою OutOfMemoryError: PermGen space.

З виходом Java 8 з’явилася нова, гнучкіша область — Metaspace. Вона замінила стару PermGen і тепер може автоматично розширюватися, займаючи стільки пам’яті, скільки потрібно системі (в межах доступної фізичної).

PermGen (до Java 8)

  • У PermGen зберігалися метадані класів.
  • Розмір PermGen був обмежений (типово невеликий), можна було збільшити параметром -XX:MaxPermSize=256m.
  • Якщо в застосунок динамічно підвантажувалося багато класів (наприклад, у веб‑серверах), PermGen міг «закінчитися», і ви отримували помилку:
java.lang.OutOfMemoryError: PermGen space
  • Проблема: очищення PermGen відбувалося не завжди коректно, якщо класи вивантажувалися динамічно (наприклад, під час перезавантаження веб‑застосунків).

Metaspace (Java 8+)

  • Починаючи з Java 8, PermGen зник, його замінив Metaspace.
  • Metaspace зберігає метадані класів, але тепер у нативній пам’яті (поза межами Java Heap).
  • Розмір Metaspace зазвичай не обмежений (обмежується лише пам’яттю системи), але можна задати ліміт через -XX:MaxMetaspaceSize=512m.
  • У разі нестачі пам’яті помилка виглядає так:
java.lang.OutOfMemoryError: Metaspace
  • У Metaspace містяться метадані класів.

Схема: як усе влаштовано

┌───────────────────────────────┐
│ JVM-процес                   │
│ ┌─────────────┐               │
│ │  Stack      │ ← Локальні змінні, виклики методів
│ └─────────────┘               │
│ ┌─────────────┐               │
│ │  Heap       │ ← Об’єкти, масиви, усе, що через new
│ └─────────────┘               │
│ ┌───────────────┐             │
│ │ MetaSpace     │ ← Метадані класів, статичні поля
│ └───────────────┘             │
└───────────────────────────────┘

Чому це важливо?

Якщо ви пишете звичайні десктопні або серверні застосунки, найімовірніше, ви ніколи не зіткнетеся з помилками PermGen або Metaspace. Але якщо працюєте з динамічним завантаженням класів (наприклад, плагіни, веб‑застосунки, фреймворки на кшталт Spring, які можуть підвантажувати та вивантажувати купу класів), то знання про Metaspace — must‑have!

5. Ілюстрація: Схема пам’яті JVM

flowchart TD
    subgraph JVM
        direction TB
        Stack1["Stack (Thread 1)"]
        Stack2["Stack (Thread 2)"]
        Heap[Heap]
        MetaSpace[MetaSpace]
    end
    Stack1 --посилається на--> Heap
    Stack2 --посилається на--> Heap
    Heap --використовує класи з--> MetaSpace
  • Кожен потік — свій стек.
  • Усі стеки можуть посилатися на об’єкти в купі.
  • Об’єкти в купі «знають» свій клас, інформація про який лежить у Metaspace.

6. Приклад: як це виглядає в реальному коді

public class MemoryDemo {
    public static void main(String[] args) {
        int x = 42; // x лежить у стеку main
        String s = "Hello!"; // s — посилання у стеку, об’єкт String у купі, рядковий літерал "Hello!" — у пулі рядків (heap)
        Person p = new Person("Alice"); // p — посилання у стеку, об’єкт Person у купі

        // Викличемо метод, щоб створити новий стек‑фрейм
        printPerson(p);
    }

    public static void printPerson(Person person) {
        // person — посилання у стеку printPerson
        System.out.println(person.getName());
    }
}

class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public String getName() { return name; }
}

Розбір:

  • x — локальна змінна, живе в стеку методу main.
  • s — посилання у стеку, об’єкт String у купі, а рядковий літерал "Hello!" — у пулі рядків (heap).
  • p — посилання у стеку, об’єкт Person у купі.
  • Клас Person і всі його методи/поля — у Metaspace (метадані класів).
  • Виклик printPerson(p) створює новий стек‑фрейм; усередині нього локальне посилання person указує на той самий об’єкт у купі.

7. Як JVM керує пам’яттю: короткий FAQ

Чи можу я керувати стеком?
Ні, стек повністю під контролем JVM. Ви можете лише задавати його розмір під час запуску (-Xss).

Чи можу я керувати купою?
Частково: розмір купи задається під час запуску (-Xmx, -Xms). За очищення відповідає збирач сміття (GC).

Чи можу я керувати Metaspace?
Можна обмежити розмір (-XX:MaxMetaspaceSize), але зазвичай це не потрібно.

Що відбувається за нестачі пам’яті?
— Якщо закінчився стек — StackOverflowError.
— Якщо закінчилася купа — OutOfMemoryError: Java heap space.
— Якщо закінчився Metaspace — OutOfMemoryError: Metaspace.

8. Типові помилки під час роботи з пам’яттю

Помилка № 1: StackOverflowError через нескінченну рекурсію. Найчастіша причина — забули передбачити умову виходу з рекурсії. Наприклад, метод викликає сам себе без зупинки. JVM не зможе розширити стек до безкінечності, і програма «впаде».

Помилка № 2: OutOfMemoryError через переповнення купи. Якщо ви створюєте надто багато об’єктів, на які продовжують посилатися змінні/колекції (наприклад, додаєте елементи до списку, але ніколи їх не видаляєте), купа може закінчитися.

Помилка № 3: OutOfMemoryError: PermGen space / Metaspace. Якщо ви використовуєте плагіни або динамічно підвантажуєте купу класів, а Metaspace не очищається (наприклад, через неправильне вивантаження класів), може закінчитися місце в Metaspace.

Помилка № 4: Плутанина між посиланням і об’єктом. Багато початківців плутають: змінна типу Person у стеку — це лише посилання, а сам об’єкт — у купі.

Помилка № 5: Очікування, що збирач сміття видалить усе миттєво. GC працює «за настроєм» (насправді — за внутрішніми алгоритмами та за нестачі пам’яті), а не одразу після того, як ви обнулили посилання. Не варто розраховувати на негайне звільнення пам’яті.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ