Вопрос 1 · Раздел 3

В чём разница между Heap и Stack?

4. TLAB — аллокация в Heap может быть быстрее Stack! 5. Virtual Threads (Java 21+) для high-concurrency 6. StackWalker (Java 9+) для анализа стека 7. Monitorьте -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 для long-running серверов (предотвращает resizing 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. Monitorьте -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]]