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

В чому різниця між Heap та Stack?

4. TLAB — алокація в Heap може бути швидшою за Stack! 5. Virtual Threads (Java 21+) для high-concurrency 6. StackWalker (Java 9+) для аналізу стеку 7. Моніторте -Xss — глибока р...

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

🟢 Junior Level

Heap (Купа) та Stack (Стек) — дві окремі ділянки віртуальної пам’яті, які JVM запитує у ОС.

Навіщо дві ділянки: у них різні патерни доступу. Stack — послідовний (LIFO): дані додаються та видаляються у чіткому порядку викликів методів. Heap — довільний: об’єкти живуть довго і доступні звідусіль.

Проста аналогія:

  • Stack — як ваш робочий стіл: швидкий доступ, але мало місця. Використовується для поточних завдань.
  • Heap — як склад: величезний, але до предметів потрібно йти. Використовується для зберігання всіх речей.

Основна різниця:

Stack Heap
Зберігає локальні змінні та посилання Зберігає об’єкти (new Object())
Швидкий доступ Повільніше
Маленький (1 МБ на потік) Величезний (гігабайти)
Очищується автоматично Очищується Garbage Collector
Приватний для потоку Спільний для всіх потоків

Приклад:

public void example() {
    int x = 10;           // Stack (примітив)
    String name = "Ivan"; // Stack (посилання) → "Ivan" в Heap
    User user = new User(); // Stack (посилання) → об'єкт User в Heap
}

Помилки:

  • Stack переповнений → StackOverflowError (нескінченна рекурсія)
  • Heap переповнений → OutOfMemoryError: Java heap space

🟡 Middle Level

Stack: Пам’ять виконання

Структура стекового фрейму:

Stack Frame (при виклику методу):
├── Local Variable Table  ← локальні змінні
├── Operand Stack         ← стек операндів для обчислень
├── Dynamic Linking       ← посилання на Constant Pool
└── Return Address        ← куди повернутися після методу

Життєвий цикл:

  • Створюється при виклику методу → видаляється при виході
  • Детермінований — пам’ять звільняється миттєво
  • Не потребує Garbage Collector

Параметр: -Xss (за замовчуванням 1 МБ)

Heap: Пам’ять стану

Що зберігається:

  • Усі об’єкти (new ...)
  • Масиви (навіть примітивів int[])
  • Статичні поля класів
  • String Pool (пул рядків)

Алокація:

  • TLAB (Thread Local Allocation Buffer) — приватний буфер потоку в Eden. Кожен потік алокує у своєму TLAB без синхронізації → дуже швидко.
  • Pointer Bumping — алокація = просто зсув вказівника (швидко!)

Параметри: -Xms (початковий), -Xmx (максимальний)

Escape Analysis: JIT оптимізація

Терміни:

  • TLAB (Thread Local Allocation Buffer) — приватний буфер потоку в Eden. Кожен потік алокує у своєму TLAB без синхронізації → дуже швидко.
  • Compressed OOPs (Compressed Ordinary Object Pointers) — стиснення посилань з 8 до 4 байт при Heap < 32 ГБ. Економія ~30% пам’яті.
  • Escape Analysis — аналіз JIT: чи «тікає» об’єкт з методу. Якщо ні — JIT може алокувати його на Stack або взагалі усунути (Scalar Replacement).
// JIT може вирішити, що об'єкт не "утече" з методу
// і створити його на стеку замість купи!
public Point createPoint() {
    Point p = new Point(10, 20);  // Може бути алокований на Stack!
    return p;  // Об'єкт "тікає" — JIT не може усунути алокацію.
// JIT аналізує всі шляхи виходу: якщо посилання повертається або
// зберігається в полі — об'єкт "тікає". Перевірити: -XX:+PrintEscapeAnalysis
}

// Scalar Replacement: об'єкт "розвалюється" на змінні
// Lock Elision: synchronized прибирається, якщо об'єкт тільки в одному потоці

Порівняльна таблиця

Критерій Stack Heap
Видимість Thread-local Shared
Управління Апаратне (Stack Pointer) GC
Очищення Миттєве (Stack Unwinding) Фонове (STW паузи)
Оптимізації Register Allocation TLAB, Compressed OOPs
Помилки StackOverflowError OutOfMemoryError

Project Loom (Virtual Threads, Java 21+)

Платформні потоки: стек у нативній пам'яті (1 МБ фіксовано)
Віртуальні потоки: стек у Heap як об'єкт!
  → Mount: копіюється в платформний стек
  → Unmount: копіюється назад у Heap
  → Результат: мільйони потоків!

🔴 Senior Level

Stack Frame Internal

Stack Frame Layout:
┌─────────────────────────────────────┐
│ Local Variable Table                │
│   [0] = this (для нестатичних)      │
│   [1] = param1                      │
│   [2] = param2                      │
│   ...                               │
├─────────────────────────────────────┤
│ Operand Stack (для byte-code ops)   │
│   push a, push b, iadd, store c     │
├─────────────────────────────────────┤
│ Dynamic Linking → Constant Pool     │
│ Return Address → next instruction   │
│ Exception Table Reference           │
└─────────────────────────────────────┘

Розмір фрейму обчислюється при компіляції!

TLAB (Thread Local Allocation Buffer)

Eden Space:
┌────────────────────────────────────┐
│ Thread 1: [TLAB_1] 64KB            │
│ Thread 2: [TLAB_2] 64KB            │
│ Thread 3: [TLAB_3] 64KB            │
│ ...                                │
└────────────────────────────────────┘

Алокація в TLAB:
  pointer += object_size  // Bump-the-pointer
  → 0 синхронізації!
  → Часто швидше, ніж malloc() у багатопотоковому середовищі, бо malloc вимагає синхронізації, а TLAB — ні.

Якщо об'єкт > TLAB → алокація в Eden з синхронізацією

NUMA Awareness

Багатопроцесорний сервер:
CPU 1 ── Memory A (швидка)
CPU 2 ── Memory B (швидка для CPU 2, повільна для CPU 1)

-XX:+UseNUMA → JVM розподіляє Heap по NUMA вузлах
→ Потоки на CPU 1 алокують в Memory A
→ +10-20% продуктивності на Highload!

Object Layout in Memory

Object Header (12-16 байт):
├── Mark Word (8 байт)
│     ├── Hash Code (31 біт)
│     ├── Age (4 біти) → для GC
│     ├── Lock State (2 біти)
│     └── GC bits
├── Klass Pointer (4 байти з Compressed OOPs)
│     → Вказівник на метадані класу в Metaspace
├── Instance Data (поля об'єкта)
└── Padding (вирівнювання до 8 байт)

Compressed OOPs: 32 ГБ поріг

64-бітний вказівник = 8 байт
Compressed OOPs = 4 байти (зсув × 8)

Максимальна адреса: 2^32 × 8 = 32 ГБ

→ 31 ГБ Heap швидше, ніж 33 ГБ!
→ Переступання 32 ГБ = -10-15% продуктивності
→ Економія L1/L2 кешів процесора

Future: Project Valhalla (Value Types)

// Майбутнє: зберігання за значенням, а не за посиланням
public final class Point {
    public final int x;
    public final int y;
}

// Зараз:
Point[] arr = new Point[1000];  // 1000 об'єктів в Heap + 1000 посилань

// Майбутнє (Value Types):
Point[] arr = new Point[1000];  // Зберігаються прямо в масиві!
 Без заголовків об'єктів
 Без непрямості
 Кеш-локальність × 10

Production Experience

Реальний сценарій: 32 ГБ поріг убив продуктивність

  • Сервер: 64 ГБ RAM, -Xmx48g
  • Переступили 32 ГБ → Compressed OOPs вимкнулись
  • Результат: -15% throughput, +20% latency
  • Рішення: -Xmx30g → Compressed OOPs увімкнено → все полагодилось

Best Practices

  1. -Xms = -Xmx у production для довготривалих серверів (запобігає overhead зміни розміру). Для CLI-утиліт залиште маленький -Xms для швидкого старту.
  2. Уникайте > 32 ГБ без необхідності (Compressed OOPs)
  3. Escape Analysis — пишіть так, щоб об’єкти не “утікали”
  4. TLAB — алокація в Heap може бути швидшою за Stack!
  5. Virtual Threads (Java 21+) для high-concurrency
  6. StackWalker (Java 9+) для аналізу стеку
  7. Моніторте -Xss — глибока рекурсія = StackOverflowError

Резюме для Senior

  • Stack = execution (швидкий, детермінований, thread-local)
  • Heap = data (величезний, shared, потребує GC)
  • TLAB = pointer bumping → алокація швидша malloc
  • Escape Analysis = Stack Allocation + Lock Elision
  • Compressed OOPs = 32 ГБ поріг → критично для performance
  • NUMA = розподіляйте Heap по вузлах для багатопроцесорних серверів
  • Object Layout = Mark Word + Klass Pointer + Data + Padding
  • Virtual Threads = стек у Heap → мільйони потоків

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • Stack — LIFO, thread-local, зберігає локальні змінні та посилання; Heap — shared, зберігає об’єкти та масиви
  • Stack очищується автоматично при виході з методу, Heap — Garbage Collector
  • Stack Overflow → StackOverflowError, Heap Overflow → OutOfMemoryError
  • TLAB дозволяє алокувати об’єкти в Heap без синхронізації (pointer bumping)
  • Escape Analysis (JIT) може алокувати об’єкт на Stack, якщо він не «тікає» з методу
  • Compressed OOPs: при Heap < 32 ГБ посилання стискаються з 8 до 4 байт → 31 ГБ часто швидше 33 ГБ
  • Object Layout: Mark Word (8 байт) + Klass Pointer (4 байти) + Data + Padding
  • Virtual Threads (Java 21+): стек у Heap, ~2 КБ на потік замість 1 МБ

Часті уточнюючі запитання:

  • Чому 31 ГБ Heap швидше 33 ГБ? — Compressed OOPs вимикаються вище 32 ГБ, вказівники подвоюються → більше cache miss
  • Чи може об’єкт бути створений на Stack? — Так, через Escape Analysis JIT може зробити Stack Allocation або Scalar Replacement
  • Що таке TLAB? — Thread Local Allocation Buffer, приватний буфер потоку в Eden для lock-free алокації
  • Який параметр задає розмір Stack?-Xss (за замовчуванням 1 МБ)

Червоні прапорці (НЕ говорити):

  • «Heap та Stack — це одне й те саме, просто різні назви» — це дві різні області пам’яті
  • «GC чистить Stack» — Stack очищується автоматично при виході з методу
  • «Об’єкти завжди створюються в Heap» — JIT може алокувати на Stack через Escape Analysis

Пов’язані теми:

  • [[2. Що зберігається в Heap]]
  • [[3. Що зберігається в Stack]]
  • [[4. Що таке Garbage Collection]]
  • [[8. Що таке покоління в GC (young, old, metaspace)]]
  • [[18. Що таке параметри -Xms та -Xmx]]