Память JVM: heap, stack и за что отвечает каждая область
Зачем это спрашивают
Вопрос «расскажи про устройство памяти JVM» — это не про зазубренную схему из учебника. Это проверка, понимаешь ли ты, почему StackOverflowError и OutOfMemoryError — разные ошибки из разных областей памяти, почему утечка возможна в языке со сборщиком мусора, и как размер стека влияет на глубину рекурсии. Если путаешь heap и stack — дальше про GC и многопоточность разговаривать бессмысленно.
Основные области памяти
JVM делит память на несколько зон, каждая со своим назначением и жизненным циклом.
- Heap — куча, общая для всех потоков. Здесь живут все объекты и массивы, создаваемые через
new. Делится на молодое (Young, с Eden и двумя Survivor-зонами) и старое (Old/Tenured) поколения. Управляется GC. - Stack — у каждого потока свой стек вызовов. Хранит фреймы методов: локальные переменные примитивных типов, ссылки на объекты в куче, адрес возврата. Удаляется автоматически при выходе из метода, GC сюда не заходит.
- Metaspace (до Java 8 — PermGen) — метаданные классов: байткод методов, пул констант, информация о структуре классов. Растёт при динамической генерации классов (проксирование, CGLIB).
- PC Register — указатель на текущую выполняемую инструкцию в потоке.
- Native memory / direct buffers — память вне управления GC: JNI,
ByteBuffer.allocateDirect, память самой JVM под структуры.
Что именно лежит в стеке
Частая путаница: «примитивы в стеке, объекты в куче». Это правда лишь отчасти. В стеке лежат локальные переменные метода — и если это примитив (int, boolean), в стеке хранится само значение. Если это ссылка на объект, в стеке хранится сама ссылка (адрес), а объект — в куче.
void method() {
int x = 5; // значение 5 — в стеке фрейма
String s = new String("hi"); // ссылка s — в стеке, объект String — в куче
}
Поля объектов (instance-переменные) всегда лежат в куче вместе с объектом, независимо от того, примитив это или ссылка.
StackOverflowError vs OutOfMemoryError
Это классический вопрос на понимание разделения областей.
StackOverflowError— переполнение стека потока. Типичная причина — бесконечная или слишком глубокая рекурсия. Размер стека фиксирован на поток (настраивается через-Xss), и каждый вызов метода добавляет фрейм.OutOfMemoryError: Java heap space— куча заполнена, GC не смог освободить достаточно памяти. Причины: утечка памяти (объекты держатся ссылками дольше, чем нужно), или просто данных больше, чем выделено (-Xmx).OutOfMemoryError: Metaspace— переполнены метаданные классов. Частая причина в проде — динамическая генерация классов без выгрузки (например, частые перезагрузки контекста Spring с CGLIB-проксями в тестах или при хот-деплое).
Как возникает утечка памяти в managed-языке
GC удаляет только недостижимые объекты. Если на объект остаётся живая ссылка — пусть даже забытая — GC его не тронет. Классические источники: статические коллекции, в которые только добавляют и никогда не чистят; незакрытые слушатели событий; ThreadLocal, не вычищенный после завершения задачи в пуле потоков.
private static final List<byte[]> cache = new ArrayList<>();
void leak() {
cache.add(new byte[1024 * 1024]); // никто не удаляет — растёт вечно
}
-Xss глобально или передав размер в конструктор Thread. Нужно для глубокой рекурсии или парсеров со вложенными структурами, где глубина вызовов превышает дефолтные ~512 КБ–1 МБ.Unsafe/malloc, минуя heap и GC-паузы на копирование. Опасность — GC не видит эту память напрямую и может не успеть освободить native-ресурс через Cleaner, что приводит к OOM за пределами heap, невидимому в обычных дампах кучи.