Що зберігається в Heap
4. Статичні змінні класів: починаючи з Java 8, статичні поля перенесені з PermGen у купу і зберігаються всередині дзеркального об'єкта java.lang.Class. 5. Класи-обгортки над при...
🟢 Junior Level
Heap (Купа) — це центральна ділянка оперативної пам’яті, що виділяється віртуальною машиною Java (JVM) для зберігання всіх динамічно створюваних об’єктів та структур даних застосунку. Купа є спільною (Shared Memory) для всіх потоків: будь-який об’єкт, створений у купі, потенційно доступний кожному потоку, який має посилання на нього.
Що конкретно розміщується в Heap:
- Усі екземпляри класів: будь-які об’єкти, створені за допомогою оператора
new(new User(),new Order()). - Усі масиви: масиви об’єктів (
String[]) та масиви примітивів (int[],byte[]). У Java будь-який масив є повноцінним об’єктом і завжди живе в Heap. - Рядковий пул (String Constant Pool): рядкові літерали та рядки, що пройшли інтернування через виклик
.intern(). - Статичні змінні класів: починаючи з Java 8, статичні поля перенесені з PermGen у купу і зберігаються всередині дзеркального об’єкта
java.lang.Class. - Класи-обгортки над примітивами: екземпляри
Integer,Long,Boolean, створені явно або в процесі автобоксингу (Integer.valueOf(42)).
Головне каверзне запитання для Junior: «Де зберігаються примітиви?»
Відповідь суворо залежить від місця оголошення змінної:
- Локальна змінна методу: зберігається в Stack поточного потоку (
int count = 10;). - Поле об’єкта (Instance Field): зберігається в Heap всередині структури даних самого об’єкта (
class Person { int age = 30; }). - Елемент масиву примітивів: зберігається в Heap у неперервній області пам’яті масиву (
int[] numbers = new int[5];).
public class MemoryAllocationDemo {
private int id = 100; // Heap (поле екземпляра класу)
public void process() {
int localVal = 42; // Stack (локальний примітив)
Point point = new Point(1, 2); // Посилання point у Stack -> об'єкт Point у Heap
int[] data = new int[]{10, 20}; // Посилання data у Stack -> масив int[] у Heap
}
}
🟡 Middle Level
Внутрішня анатомія об’єкта в пам’яті (Object Layout)
Кожен об’єкт, розміщений у купі 64-бітної JVM HotSpot, має суворо регламентовану структуру:
┌────────────────────────────────────────────────────────┐
│ OBJECT HEADER │
├────────────────────────────────────────────────────────┤
│ 1. Mark Word (8 байтів) │
│ - Identity HashCode, Age bits (GC), Стан блокування │
├────────────────────────────────────────────────────────┤
│ 2. Klass Word (4 байти при Compressed Class Pointers) │
│ - Посилання на метадані класу в Metaspace │
├────────────────────────────────────────────────────────┤
│ 3. Array Length (4 байти, присутнє тільки в масивів) │
├────────────────────────────────────────────────────────┤
│ 4. Instance Data (значення полів об'єкта) │
│ - Примітивні типи та посилання на інші об'єкти │
├────────────────────────────────────────────────────────┤
│ 5. Padding (Вирівнювання) │
│ - Доповнення нулями до кратності 8 байтам │
└────────────────────────────────────────────────────────┘
Правило перевпорядкування полів (Field Packing)
Для мінімізації фрагментації та оверхеду вирівнювання JVM перевпорядковує поля об’єкта не в порядку їх оголошення в Java-коді, а за розміром типів:
double/long(8 байтів)int/float(4 байти)short/char(2 байти)byte/boolean(1 байт)- Посилання на об’єкти (OOP — 4 або 8 байтів)
String Pool та Compact Strings (Java 9+)
- Історія розміщення String Pool:
- До Java 7: String Pool розташовувався в PermGen (Permanent Generation). Це призводило до частих помилок
java.lang.OutOfMemoryError: PermGen spaceу разі динамічної генерації рядків. - Починаючи з Java 7: String Pool перенесено в Java Heap, завдяки чому невикористовувані інтерновані рядки своєчасно очищаються звичайним Garbage Collector.
- До Java 7: String Pool розташовувався в PermGen (Permanent Generation). Це призводило до частих помилок
- Compact Strings (JEP 254 у Java 9):
- До Java 9:
java.lang.Stringусередині містив масивchar[] value, де кожен символ займав 2 байти (UTF-16), навіть якщо рядок складався виключно з ASCII-символів. - З Java 9: рядки перейшли на
byte[] valueта 1-байтний прапорець кодуванняbyte coder. Якщо рядок містить лише символи Latin-1 (ISO-8859-1), кожен символ займає 1 байт, що заощаджує до 15–20% пам’яті купи в enterprise-застосунках.
- До Java 9:
Стиснені покажчики: Compressed OOPs
На 64-бітних JVM адреса покажчика на об’єкт має займати 8 байтів. Проте за обсягу купи менше 32 ГБ HotSpot за замовчуванням вмикає прапорець -XX:+UseCompressedOops:
- Оскільки об’єкти вирівнюються з кроком у 8 байтів, молодші 3 біти адреси завжди нульові.
- JVM відкидає 3 нулі, стискаючи посилання до 4 байтів.
- $2^{32} \times 8 \text{ байтів} = 32 \text{ ГБ}$ — граничний обсяг адресованої пам’яті зі стисненими покажчиками.
💡 Ефект порогу 32 ГБ: Збільшення
-Xmxз 31 ГБ до 33 ГБ вимикає Compressed OOPs. Усі посилання збільшуються з 4 до 8 байтів, що з’їдає близько 3–4 ГБ пам’яті на самі лише покажчики та знижує щільність влучання в кеш процесора L1/L2/L3.
🔴 Senior Level
Shallow Size vs Retained Size
Під час профілювання витоків пам’яті та аналізу Heap Dump (через Eclipse MAT або JProfiler) ключове значення має розрізнення двох метрик розміру об’єктів:
┌────────────────────────────────────────────────────────┐
│ Shallow Size = Власна пам'ять об'єкта │
│ (Header + власні поля + padding) │
├────────────────────────────────────────────────────────┤
│ Retained Size = Shallow Size │
│ + Пам'ять УСІХ об'єктів, досяжних ВИКЛЮЧНО │
│ через цей об'єкт (Dominator Tree) │
└────────────────────────────────────────────────────────┘
Приклад: Порожній список new ArrayList<>(100):
- Shallow Size: заголовок (12 байтів) + посилання на внутрішній масив (4 байти) +
int size(4 байти) + padding (4 байти) = 24 байти. - Retained Size: власні 24 байти + розмір внутрішнього масиву
Object[100](424 байти) = 448 байтів. Якщо в список додати 100 об’єктівUser, які більше ніким у застосунку не використовуються, їхній обсяг також увійде в Retained Size цього списку.
Великі об’єкти: Humongous Objects у збирачі G1
У garbage collector G1 (що використовується за замовчуванням) купа розбита на тисячі однакових регіонів (від 1 МБ до 32 МБ залежно від загального розміру купи).
- Якщо розмір об’єкта перевищує 50% від розміру регіону G1, він класифікується як Humongous Object.
- Особливості розміщення Humongous об’єктів:
- Вони алокуються не в Eden, а безпосередньо в спеціальну неперервну послідовність регіонів в Old Generation.
- Не копіюються збирачем сміття під час Young GC (усувається оверхед на переміщення важких масивів).
- Можуть викликати сильну фрагментацію купи: якщо бракує достатньої кількості вільних суміжних регіонів, JVM ініціює дострокове глобальне збирання сміття Full GC.
Аналіз розподілу пам’яті через JOL (Java Object Layout)
Інструмент org.openjdk.jol:jol-core дає змогу дослідити реальну розкладку байтів у пам’яті:
import org.openjdk.jol.info.ClassLayout;
public class JolInspection {
public static void main(String[] args) {
System.out.println(ClassLayout.parseClass(Order.class).toPrintable());
}
}
class Order {
private long id; // 8 байтів
private int status; // 4 байти
private boolean paid; // 1 байт
private Object payload;// 4 байти (стиснене посилання)
}
Виведення JOL на 64-бітній JVM:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 8 (object header: mark word) ...
8 4 (object header: class word) ...
12 4 int Order.status 0
16 8 long Order.id 0
24 4 Object Order.payload null
28 1 boolean Order.paid false
29 3 (alignment/padding gap)
Instance size: 32 bytes
Зверніть увагу: JVM переставила поле status перед полем id, щоб закрити 4-байтний проміжок після заголовка (12 байтів + 4 байти int = вирівнювання за межею 8 байтів).
Off-Heap пам’ять: коли купи недостатньо
Під час роботи з десятками гігабайтів кешу (in-memory data grids, брокери повідомлень Kafka, Cassandra) алокація в Heap призводить до тривалих пауз GC. Для розв’язання цієї проблеми застосовують Off-Heap пам’ять (поза купою):
java.nio.ByteBuffer.allocateDirect(): пам’ять виділяється ОС безпосередньо через системний викликmalloc().- Project Panama (Foreign Function & Memory API, Java 22+): інтерфейси
ArenaтаMemorySegmentзабезпечують безпечну, детерміновану роботу з нативною пам’яттю без оверхеду GC.
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь (Elevator Pitch)
«У Heap зберігаються всі динамічні дані застосунку: екземпляри класів, створені через
new, масиви будь-якого типу (включно з примітивними), рядковий пул (String Pool) та статичні поля класів (всередині об’єктівClassз Java 8). Анатомія будь-якого об’єкта в купі складається із заголовка (Mark Word + Klass Word), полів даних і байтів вирівнювання (Padding). За обсягу купи до 32 ГБ JVM вмикає Compressed OOPs, стискаючи посилання до 4 байтів. Під час діагностики витоків критично важливо розрізняти Shallow Size (власна вага об’єкта) та Retained Size (вага об’єкта з усім деревом залежностей, яку звільнить GC)».
3–4 каверзних запитання з відповідями
- Де зберігаються примітивні змінні: в Heap чи в Stack?
- Відповідь: Локальні примітивні змінні всередині методів зберігаються у стековому фреймі поточного потоку в Stack. Проте примітивні поля об’єктів зберігаються в купі (Heap) безпосередньо всередині тіла відповідного об’єкта. Елементи масивів примітивів (
byte[],int[]) також завжди розміщуються в Heap.
- Відповідь: Локальні примітивні змінні всередині методів зберігаються у стековому фреймі поточного потоку в Stack. Проте примітивні поля об’єктів зберігаються в купі (Heap) безпосередньо всередині тіла відповідного об’єкта. Елементи масивів примітивів (
- Де зберігаються статичні змінні класів?
- Відповідь: Починаючи з Java 8, статичні змінні зберігаються в Heap. Вони розміщуються як поля всередині дзеркального об’єкта
java.lang.Class, який перебуває в купі. У нативній пам’яті Metaspace зберігаються лише метадані класу (байткод методів, пул констант, анотації).
- Відповідь: Починаючи з Java 8, статичні змінні зберігаються в Heap. Вони розміщуються як поля всередині дзеркального об’єкта
- У чому різниця між Shallow Size та Retained Size об’єкта?
- Відповідь: Shallow Size — це розмір оперативної пам’яті, займаний виключно самим об’єктом (його заголовок, примітивні поля, посилання та padding). Retained Size — це сумарний обсяг пам’яті, який буде звільнено збирачем сміття, якщо видалити цей об’єкт (тобто його власний shallow size плюс розмір усіх об’єктів, досяжних виключно через нього).
- Що таке Humongous об’єкти в збирачі сміття G1 і чому вони небезпечні?
- Відповідь: У G1 об’єкт вважається Humongous, якщо його розмір перевищує 50% від розміру регіону G1. Такі об’єкти алокуються безпосередньо в спеціальний неперервний набір регіонів в Old Generation, оминаючи Eden та Survivor. Вони небезпечні тим, що спричиняють фрагментацію купи: якщо для нового великого масиву не знайдеться неперервної послідовності вільних регіонів, JVM ініціює примусовий Full GC зі Stop-The-World паузою.
Типові помилки та Red Flags
- 🚩 Твердження: «Усі примітиви завжди зберігаються тільки в Stack» (класична помилка: примітивні поля класів живуть у Heap).
- 🚩 Омана: «String Pool розташований у PermGen / Metaspace» (String Pool було перенесено в Heap ще в Java 7).
- 🚩 Нерозуміння різниці між Shallow та Retained Size під час розслідування витоків пам’яті.
- 🚩 Ігнорування порогу 32 ГБ купи (встановлення
-Xmx33gпризводить до вимкнення Compressed OOPs і падіння ефективності кешу процесора).
Пов’язані теми
- В чому різниця між Heap та Stack — фундаментальне порівняння двох областей пам’яті
- Що зберігається в Stack — детальний розбір структури стекового фрейму
- Що таке витік пам’яті в Java — діагностика накопичених об’єктів у Heap
- Що таке покоління в GC — Eden, Survivor, Old Gen
- Що таке Metaspace (або PermGen) — розділення купи та нативної пам’яті метаданих