Питання 2 · Розділ 3

Що зберігається в Heap?

4. String Pool економить пам'ять на рядках 5. Static collections — головне джерело витоків 6. NUMA вмикайте на багатопроцесорних серверах. Не вмикайте на однопроцесорних — оверх...

Мовні версії: English Russian Ukrainian

🟢 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 ГБ без

Типові помилки

  1. Зберігання занадто багато в Heap
    // ❌ Кеш без ліміту
    static Map<String, Object> cache = new HashMap<>();
    cache.put(key, value);  // Росте нескінченно → OOM!
    
  2. Створення зайвих об’єктів
    // ❌ Сміття в Heap
    String s = new String("hello");  // Два об'єкти:
    // 1. Літерал "hello" в String Pool (об'єкт #1)
    // 2. new String() — обгортка поверх літералу (об'єкт #2)
    
    // ✅
    String s = "hello";  // Один об'єкт з пулу
    
  3. Витоки через статичні колекції
    // ❌ 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

  1. Уникайте > 32 ГБ без необхідності (Compressed OOPs)
  2. Групуйте поля за розміром для вирівнювання
  3. TLAB — алокація об’єктів майже безкоштовна
  4. String Pool економить пам’ять на рядках
  5. Static collections — головне джерело витоків
  6. NUMA вмикайте на багатопроцесорних серверах. Не вмикайте на однопроцесорних — оверхед без користі.
  7. 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)]]