Що зберігається в Stack?
4. Virtual Threads (Java 21+) для high-concurrency. Не використовуйте для CPU-bound задач — вони не дають приросту і додають оверхед на монтування/демонтування. 5. Escape Analys...
🟢 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));
Типові помилки
- Глибока рекурсія
// ❌ 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- Рішення:
- Збільшити
-Xssдо 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
- Stream API для складних трансформацій
- StackWalker для аналізу ланцюжка викликів
- Уникайте глибокої рекурсії — використовуйте ітерацію
- Virtual Threads (Java 21+) для high-concurrency. Не використовуйте для CPU-bound задач — вони не дають приросту і додають оверхед на монтування/демонтування.
- 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]]