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

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

4. Virtual Threads (Java 21+) для high-concurrency. Не використовуйте для CPU-bound задач — вони не дають приросту і додають оверхед на монтування/демонтування. 5. Escape Analys...

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

🟢 Junior Level

Stack (Стек) — приватна область пам’яті для кожного потоку.

Навіщо свій стек кожному потоку: у кожного потоку свій ланцюжок викликів методів. Потоки не можуть спільно використовувати один Stack — інакше локальні змінні одного потоку були б видимі іншому.

Проста аналогія: Стопка тарілок. Коли викликаєте метод — кладете тарілку зверху. Коли метод завершується — забираєте тарілку. Завжди працюєте з верхньою тарілкою.

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

  • Примітиви: int x = 10;, double price = 99.9;
  • Посилання на об’єкти: User user = ...; (посилання в Stack, об’єкт в Heap)
  • Інформація про виклики методів

Приклад:

public void methodA() {
    int a = 5;         // Stack
    methodB();         // Новий фрейм на стеку
}

public void methodB() {
    int b = 10;        // Stack (у своєму фреймі)
    // Коли метод завершиться → b видалиться
}

Розмір Stack: -Xss (за замовчуванням 1 МБ) Помилка: StackOverflowError при нескінченній рекурсії


🟡 Middle Level

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

Stack Frame (при виклику методу):
┌─────────────────────────────────┐
│ Local Variable Table            │
│   [0] = this                    │
│   [1] = param1                  │
│   [2] = param2                  │
│   [3] = local var               │
├─────────────────────────────────┤
│ Operand Stack                   │
│   Для обчислень: push, pop      │
│   a + b → push a, push b, add   │
├─────────────────────────────────┤
│ Dynamic Linking                 │
│   → Constant Pool (методи, поля)│
├─────────────────────────────────┤
│ Return Address                  │
│   → Куди повернутися            │
└─────────────────────────────────┘

Що саме в Stack

public class Example {
    public void process(int x) {
        int local = x * 2;        // Stack (примітив)
        String s = "hello";       // Stack (посилання) → "hello" в Heap
        User user = new User();   // Stack (посилання) → об'єкт в Heap
        int[] arr = new int[10];  // Stack (посилання) → масив в Heap
    }
}

ВАЖЛИВО: У Stack зберігаються тільки посилання на об’єкти, не самі об’єкти!

StackWalker API (Java 9+)

// Застарілий спосіб (важкий)
StackTraceElement[] stack = new Throwable().getStackTrace();

// Сучасний спосіб (лінивий, швидкий)
StackWalker walker = StackWalker.getInstance();
walker.forEach(frame -> System.out.println(frame.getClassName()));

// З фільтрацією
String caller = StackWalker.getInstance()
    .walk(frames -> frames
        .skip(1)  // Пропустити поточний метод
        .findFirst()
        .map(StackWalker.StackFrame::getClassName)
        .orElse(null));

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

  1. Глибока рекурсія
    // ❌ StackOverflowError
    public int factorial(int n) {
        return n * factorial(n - 1);  // Немає базового випадку!
    }
    

🔴 Senior Level

Stack Frame: Under the Hood

JVM Specification:
  Фрейм створюється при виклику методу
  Розмір обчислюється при компіляції (відома к-сть локальних змінних)

Local Variable Table:
  - Масив слів (32 біти кожне)
  - long/double займають 2 слоти
  - Індекс 0 = 'this' для нестатичних методів
  - Параметри методів йдуть першими

Operand Stack:
  - Глибина теж відома при компіляції
  - Використовується для byte-code інструкцій
  - Приклад: a + b + c
    → push a → push b → iadd → push c → iadd

Virtual Threads та Stack (Java 21+)

Платформні потоки (до Java 21):
  Stack = фіксований шматок нативної пам'яті (1 МБ через -Xss)
  → Обмеження: ~1000 потоків на 1 ГБ RAM
  → Не можна змінити розмір на льоту

Віртуальні потоки (Java 21+):
  Stack = об'єкт в Heap!
  → Mount: копіюється в Carrier Thread stack
  → Yield (блокування): копіюється назад у Heap
  → Результат: мільйони потоків!

Пам'ять на віртуальний потік:
  → ~2 КБ — початкове значення для порожнього віртуального потоку. Стек росте динамічно при заглибленні викликів.
  → Динамічно росте за необхідності

Tail Call Optimization (TCO)

// Хвостова рекурсія
public int sum(int n, int acc) {
    if (n == 0) return acc;
    return sum(n - 1, acc + n);  // Хвостовий виклик
}

// У мовах з TCO → перевикористовується той самий фрейм
// Java НЕ підтримує TCO напряму!
// Але JIT робить Method Inlining:

// Після Inlining:
public int sum(int n, int acc) {
    while (n != 0) {
        acc += n;
        n--;
    }
    return acc;
}
// → 1 фрейм замість N!

Stack та Escape Analysis

// JIT може алокувати об'єкт на Stack!
public void process() {
    Point p = new Point(10, 20);  // Зазвичай в Heap

    // Якщо JIT бачить, що 'p' не "утече" з методу:
    // 1. Scalar Replacement: розвалити на int x=10, y=20
    // 2. Stack Allocation: створити на Stack
    // 3. Lock Elision: прибрати synchronized якщо один потік

    System.out.println(p.x + p.y);  // Використовується тільки тут
}

// Перевірка: -XX:+PrintEscapeAnalysis -XX:+DoEscapeAnalysis

Stream API як Iterator

// Stream = просунутий Iterator з lazy evaluation
list.stream()
    .filter(s -> s.startsWith("A"))   // Не виконується поки немає terminal
    .map(String::toUpperCase)         // Не виконується
    .forEach(System.out::println);    // Terminal → запускає pipeline

// На відміну від Iterator:
// 1. Composability — можна будувати ланцюжки
// 2. Lazy — обчислення тільки коли потрібні
// 3. Parallel — автоматичний паралелізм
// 4. Functional — без побічних ефектів

Production Experience

Реальний сценарій: StackOverflowError у production

  • Deep nested JSON parsing → рекурсія 10,000+ рівнів
  • -Xss = 1 МБ → StackOverflowError
  • Рішення:
    1. Збільшити -Xss до 2 МБ (тимчасове)
    2. Переписати парсер на ітеративний (постійне)

Реальний сценарій: Virtual Threads врятували сервер

  • REST API: 10,000 concurrent запитів
  • Platform threads: 10,000 × 1 МБ = 10 ГБ RAM на стеки
  • Virtual threads: 10,000 × ~2 КБ = 20 МБ в Heap
  • Економія: 99.8% RAM!

Best Practices

  1. Stream API для складних трансформацій
  2. StackWalker для аналізу ланцюжка викликів
  3. Уникайте глибокої рекурсії — використовуйте ітерацію
  4. Virtual Threads (Java 21+) для high-concurrency. Не використовуйте для CPU-bound задач — вони не дають приросту і додають оверхед на монтування/демонтування.
  5. Escape Analysis допомагає JIT оптимізувати алокації

Резюме для Senior

  • Stack = fragmented execution memory, frame-per-frame
  • Virtual Threads = стек у Heap → dynamic sizing → millions of threads. Не для CPU-bound задач.
  • StackWalker = lazy stack inspection, +performance
  • Escape Analysis = Stack Allocation + Lock Elision
  • TCO не підтримується, але Method Inlining вирішує
  • Stream API = lazy Iterator з composability

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

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

  • Stack зберігає: локальні примітиви, посилання на об’єкти, інформацію про виклики методів
  • Кожен потік має свій Stack (за замовчуванням 1 МБ, параметр -Xss)
  • Стековий фрейм: Local Variable Table + Operand Stack + Dynamic Linking + Return Address
  • При виході з методу фрейм видаляється миттєво — GC не потрібен
  • Virtual Threads (Java 21+): стек у Heap як об’єкт, ~2 КБ замість 1 МБ
  • JIT може алокувати об’єкт на Stack через Escape Analysis, якщо він не «тікає»
  • Java не підтримує Tail Call Optimization напряму, але JIT робить Method Inlining

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

  • Що буде при нескінченній рекурсії?StackOverflowError, Stack переповнений
  • Чому Virtual Threads дозволяють мільйони потоків? — Стек у Heap, динамічний розмір (~2 КБ vs 1 МБ фіксовано)
  • Чи можна змінити розмір Stack на льоту? — Ні, -Xss задається при старті JVM. Virtual Threads вирішують це динамічним розміром.
  • Що таке Operand Stack? — Стек операндів всередині фрейму для byte-code інструкцій (push/pop/add)

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

  • «Об’єкти зберігаються в Stack» — у Stack зберігаються тільки посилання на об’єкти
  • «Stack спільний для всіх потоків» — Stack приватний для кожного потоку
  • «Java підтримує TCO» — Java НЕ підтримує Tail Call Optimization

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

  • [[1. В чому різниця між Heap та Stack]]
  • [[2. Що зберігається в Heap]]
  • [[4. Що таке Garbage Collection]]
  • [[16. Що таке stop-the-world]]
  • [[18. Що таке параметри -Xms та -Xmx]]