Что хранится в Stack?
4. Virtual Threads (Java 21+) для high-concurrency. Не используйте для CPU-bound задач — они не дают прироста и добавляют оверхед на монтирование/демонтирование. 5. Escape Analy...
🟢 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 НЕ поддерживает T напрямую!
// Но 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]]