Що зберігається в Heap?
4. String Pool економить пам'ять на рядках 5. Static collections — головне джерело витоків 6. NUMA вмикайте на багатопроцесорних серверах. Не вмикайте на однопроцесорних — оверх...
🟢 Junior Level
Heap (Купа) — область віртуальної пам’яті (від десятків МБ до терабайт), виділена JVM для зберігання об’єктів. Відокремлена від Stack, тому що об’єкти живуть довше за виклики методів.
Що саме:
- Все, що створено через
new→ в Heap - Масиви → в Heap
- Рядки → в Heap (String Pool)
- Статичні поля → в Heap
Приклад:
public class Example {
static int count = 0; // Static field → в Heap
public void method() {
int x = 10; // Примітив → в Stack
String s = "hello"; // Посилання в Stack, "hello" → в Heap
User user = new User(); // Посилання в Stack, об'єкт → в Heap
int[] arr = new int[5]; // Посилання в Stack, масив → в Heap
}
}
Розмір Heap:
- Налаштовується:
-Xms(початковий),-Xmx(максимальний) - За замовчуванням: залежить від системи (зазвичай 1/4 RAM)
- Помилка при переповненні:
OutOfMemoryError: Java heap space
🟡 Middle Level
Що зберігається в Heap
1. Інстанси об’єктів:
new User("Ivan") // Об'єкт User в Heap
new ArrayList<>() // Об'єкт ArrayList + внутрішні масиви
2. Масиви (навіть примітивів):
int[] numbers = new int[1000]; // Масив — об'єкт → в Heap
byte[] data = new byte[1024]; // Навіть byte[] — об'єкт в Heap
3. Статичні поля (з Java 8):
public class Config {
private static String DB_URL = "jdbc:..."; // В Heap (об'єкт Class)
}
4. String Pool:
String s1 = "hello"; // В String Pool (частина Heap)
String s2 = "hello"; // Те саме посилання (пул економить пам'ять)
Анатомія об’єкта в пам’яті
Object в Heap:
┌─────────────────────────┐
│ Header (12-16 байт) │
│ Mark Word (8 байт) │
│ Klass Pointer (4 байти)│
├─────────────────────────┤
│ Instance Data (поля) │
│ int age = 25 (4 байти)│
│ String name (4 байти - посилання)│
├─────────────────────────┤
│ Padding (до 8 байт) │
└─────────────────────────┘
Mark Word зберігає:
- Hash code об’єкта
- Age для GC (скільки зборок пережив)
- Стан блокувань
Klass Pointer:
- Вказівник на метадані класу в Metaspace
- 4 байти з Compressed OOPs (< 32 ГБ Heap)
- 8 байт без Compressed OOPs (> 32 ГБ Heap)
Compressed OOPs оптимізація
32-бітна JVM: вказівники = 4 байти
64-бітна JVM: вказівники = 8 байт
Compressed OOPs (32 ГБ > Heap):
Вказівник = 4 байти (зсув × 8)
→ Економія 50% пам'яті на посиланнях!
→ Більше даних у кеші процесора
Поріг: 32 ГБ Heap
2^32 × 8 байт = 32 ГБ
Чому 31 ГБ швидше 33 ГБ:
- 31 ГБ → Compressed OOPs увімкнено → -50% пам’яті на посилання
- 33 ГБ → Compressed OOPs вимкнено → більше RAM, але повільніше
- Часто 31 ГБ з Compressed OOPs швидше, ніж 33 ГБ без
Типові помилки
- Зберігання занадто багато в Heap
// ❌ Кеш без ліміту static Map<String, Object> cache = new HashMap<>(); cache.put(key, value); // Росте нескінченно → OOM! - Створення зайвих об’єктів
// ❌ Сміття в Heap String s = new String("hello"); // Два об'єкти: // 1. Літерал "hello" в String Pool (об'єкт #1) // 2. new String() — обгортка поверх літералу (об'єкт #2) // ✅ String s = "hello"; // Один об'єкт з пулу - Витоки через статичні колекції
// ❌ Static колекція росте нескінченно static List<String> history = new ArrayList<>(); history.add(data); // Ніколи не очищується!
🔴 Senior Level
Heap структура (HotSpot JVM)
Heap Layout:
┌──────────────────────────────────────────┐
│ Young Generation │
│ Eden (80%) │
│ Survivor 0 (10%) │
│ Survivor 1 (10%) │
├──────────────────────────────────────────┤
│ Old Generation (Tenured) │
│ Довгоживучі об'єкти │
├──────────────────────────────────────────┤
│ Humongous Regions (G1) │
│ Об'єкти > 50% регіону │
└──────────────────────────────────────────┘
TLAB (Thread Local Allocation Buffer)
Проблема: сотні потоків алокують одночасно → contention
Рішення: кожному потоку свій шматок Eden
Thread 1: TLAB[64KB] → pointer bumping
Thread 2: TLAB[64KB] → pointer bumping
Thread 3: TLAB[64KB] → pointer bumping
Алокація в TLAB:
obj_ptr = thread.tlab_ptr
thread.tlab_ptr += obj_size
→ 0 синхронізації!
→ Швидше ніж malloc() в C++!
PLAB (Promotion Local Allocation Buffer):
→ Аналог TLAB для Old Gen
→ Використовується при копіюванні об'єктів, що вижили
NUMA-Aware Heap
NUMA (Non-Uniform Memory Access):
CPU 1 ── Local Memory (швидка, 100ns)
CPU 2 ── Remote Memory (повільна, 150ns)
-XX:+UseNUMA:
→ JVM алокує пам'ять локально для кожного CPU
→ +10-20% продуктивності на багатопроцесорних серверах
Важливо для Highload з > 2 CPU sockets
Object Layout Deep Dive
64-bit JVM, Compressed OOPs:
┌─────────────────────────────────┐
│ Mark Word (8 байт) │
│ Hash: 31 біт │
│ Age: 4 біти (max 15) │
│ Lock: 2 біти │
│ GC: 1 біт │
├─────────────────────────────────┤
│ Klass Pointer (4 байти) │
│ → Зсув × 8 → адреса в Metaspace│
├─────────────────────────────────┤
│ Array Length (4 байти) │ ← тільки для масивів
├─────────────────────────────────┤
│ Instance Data │
│ long (8 байт) │
│ int (4 байти) │
│ short (2 байти) │
│ byte (1 байт) │
│ ref (4 байти з Compressed) │
├─────────────────────────────────┤
│ Padding (до кратності 8) │
└─────────────────────────────────┘
→ Мінімальний об'єкт: 16 байт (порожній Object)
→ Integer: 24 байти (16 header + 4 int + 4 padding)
Memory Alignment
Процесор читає пам'ять словами (8 байт = 64 біти)
Невирівняний доступ:
[1234][5678][9...]
^ дані перетинають два слова → 2 цикли читання
Вирівняний доступ:
[1234][5678][9...]
^ дані в одному слові → 1 цикл читання
→ JVM автоматично доповнює до 8 байт
→ Групуйте поля: long/long → int/int → short → byte
Heap Monitoring
// Runtime API
Runtime rt = Runtime.getRuntime();
long max = rt.maxMemory(); // -Xmx
long total = rt.totalMemory(); // Поточний committed
long free = rt.freeMemory(); // Вільно
long used = total - free; // Використано
// MemoryMXBean
MemoryMXBean mem = ManagementFactory.getMemoryMXBean();
MemoryUsage heap = mem.getHeapMemoryUsage();
Future: Project Valhalla
// Value Types (майбутнє Java):
public final class Point {
public final int x;
public final int y;
}
// Зараз:
Point[] arr = new Point[100];
→ 100 об'єктів в Heap (1600 байт заголовків)
→ 100 посилань (400 байт)
→ Разом: ~4 КБ оверхеду
// З Value Types:
Point[] arr = new Point[100];
→ Дані зберігаються прямо в масиві!
→ Без заголовків, без посилань
→ Кеш-локальність × 10
Production Experience
Реальний сценарій: 33 ГБ Heap повільніше 31 ГБ
- Додаток: Spring Boot,
-Xmx33g - Compressed OOPs вимкнено → вказівники 8 байт
- L3 cache miss rate: 25%
- Рішення:
-Xmx30g→ Compressed OOPs увімкнено - Результат: L3 miss rate 12%, +15% throughput
Best Practices
- Уникайте > 32 ГБ без необхідності (Compressed OOPs)
- Групуйте поля за розміром для вирівнювання
- TLAB — алокація об’єктів майже безкоштовна
- String Pool економить пам’ять на рядках
- Static collections — головне джерело витоків
- NUMA вмикайте на багатопроцесорних серверах. Не вмикайте на однопроцесорних — оверхед без користі.
- Object Layout впливає на кеш-локальність
Резюме для Senior
- Heap зберігає: об’єкти, масиви, статичні поля, String Pool
- Object Layout = Header (Mark Word + Klass) + Data + Padding
- Compressed OOPs = 32 ГБ поріг → 50% економії на посиланнях
- TLAB = lock-free алокація → швидше malloc()
- NUMA = локальна пам’ять для кожного CPU → +10-20%
- Memory Alignment = кратність 8 байт → 1 цикл читання
- Value Types (Valhalla) = зберігання за значенням → без оверхеду
- 31 ГБ часто швидше 33 ГБ через Compressed OOPs
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- В Heap: усі об’єкти (
new), масиви (навіть примітивів), статичні поля, String Pool - Мінімальний об’єкт на 64-бітній JVM з Compressed OOPs: 16 байт (header 12 + padding)
- Compressed OOPs стискають посилання з 8 до 4 байт при Heap < 32 ГБ
- TLAB — приватний буфер потоку в Eden, алокація = pointer bumping (0 синхронізації)
- String Pool — частина Heap, рядки-літерали перевикористовуються
- Static колекції — головне джерело витоків пам’яті
- 31 ГБ Heap часто швидше 33 ГБ через Compressed OOPs
Часті уточнюючі запитання:
- Чому
new String("hello")створює 2 об’єкти? — Літерал “hello” в String Pool + обгорткаnew String() - Що зберігає Mark Word? — Hash code, age для GC, стан блокувань
- Що таке PLAB? — Promotion Local Allocation Buffer, аналог TLAB для Old Gen при копіюванні об’єктів, що вижили
- Чому статичні колекції — частий витік? — Static field = GC Root, об’єкти ніколи не будуть зібрані
Червоні прапорці (НЕ говорити):
- «Примітиви зберігаються в Heap» — локальні примітиви зберігаються в Stack
- «String Pool знаходиться в Metaspace» — з Java 7 String Pool в Heap
- «Heap чиститься автоматично при виході з методу» — Heap чистить GC, Stack чиститься при виході з методу
Пов’язані теми:
- [[1. В чому різниця між Heap та Stack]]
- [[3. Що зберігається в Stack]]
- [[6. Що таке витік пам’яті в Java]]
- [[8. Що таке покоління в GC (young, old, metaspace)]]
- [[11. Що таке Metaspace (або PermGen)]]