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 працює «за настроєм» (насправді — за внутрішніми алгоритмами та за нестачі пам’яті), а не одразу після того, як ви обнулили посилання. Не варто розраховувати на негайне звільнення пам’яті.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ