Вопросы про JVM отделяют middle от senior. От junior ждут «куча и стек», от middle устройство поколений и G1, от senior умение разобрать утечку памяти и выбрать сборщик под задачу.
Области памяти
| Область | Что там | Ошибка при переполнении |
|---|---|---|
| Heap (куча) | все объекты и массивы, общий для потоков | OutOfMemoryError: Java heap space |
| Stack потока | фреймы методов: локальные переменные, ссылки, операнды | StackOverflowError |
| Metaspace | метаданные классов (в нативной памяти с Java 8, заменил PermGen) | OutOfMemoryError: Metaspace |
| Code cache | скомпилированный JIT машинный код | JIT отключается с предупреждением |
| Direct memory | ByteBuffer.allocateDirect, буферы Netty | OutOfMemoryError: Direct buffer memory |
Локальная переменная примитивного типа лежит в стеке, объект всегда в куче, в стеке только ссылка на него. (JIT может после escape analysis вообще не создавать объект, если тот не покидает метод, но это оптимизация, а не модель.)
Размеры: -Xms/-Xmx куча, -Xss стек потока. В контейнере JVM видит лимиты cgroup, по умолчанию куча это 25% памяти контейнера, обычно задают -XX:MaxRAMPercentage=75.
Как GC находит мусор
Объект жив, если до него можно дойти по ссылкам от GC roots: локальные переменные активных фреймов, статические поля загруженных классов, активные потоки, JNI-ссылки. Всё остальное мусор, даже если объекты ссылаются друг на друга по кругу. Подсчёта ссылок в JVM нет.
Поколения
Гипотеза поколений: большинство объектов умирает молодыми. Поэтому куча делится:
- Young: Eden и два Survivor. Новые объекты создаются в Eden. Minor GC копирует живых в Survivor, после нескольких переживаний (порог возраста) объект переезжает в Old.
- Old: долгоживущие объекты, собираются реже и дороже.
Копирующая сборка молодого поколения дешёвая: стоимость пропорциональна живым, а не мёртвым.
Сборщики
| Сборщик | Когда |
|---|---|
| Serial | один поток, маленькие кучи и утилиты |
| Parallel | максимальная пропускная способность, паузы не важны (батчи) |
| G1 | по умолчанию с Java 9: куча из регионов, цель по паузе -XX:MaxGCPauseMillis (200 мс) |
| ZGC | паузы около миллисекунды на кучах до терабайт, с Java 21 есть поколенческий режим, с 23 он по умолчанию для ZGC |
| Shenandoah | низкие паузы, похож по целям на ZGC |
Stop-the-world пауза останавливает все потоки приложения. Современные сборщики делают большую часть работы конкурентно, но короткие паузы остаются.
Ссылки
- Strong: обычная ссылка, объект не собирается.
- Soft: собирается при нехватке памяти (кеши, но лучше Caffeine).
- Weak: собирается на ближайшей сборке (
WeakHashMap, ключи которого не держат объект). - Phantom: уведомление после сборки, на нём построен
Cleaner.
Утечки памяти в Java
GC собирает только недостижимое. Утечка это достижимое, но ненужное:
- статическая коллекция или кеш без ограничения размера;
ThreadLocalв пуле потоков безremove();- незакрытые ресурсы и забытые слушатели;
- изменяемые ключи в
HashMap; - загрузчик классов, удерживаемый после редеплоя (утечка Metaspace).
Диагностика: jcmd <pid> GC.heap_info, heap dump (jcmd <pid> GC.heap_dump или -XX:+HeapDumpOnOutOfMemoryError) и разбор в Eclipse MAT по dominator tree, Java Flight Recorder для профиля аллокаций, логи GC -Xlog:gc*.
Загрузка классов
Класс загружается лениво, при первом использовании. Этапы: loading (найти байткод и создать Class), linking (verification, preparation: статические поля получают нули, resolution ссылок), initialization (static-блоки и инициализаторы).
Загрузчики: Bootstrap (ядро JDK), Platform, Application (classpath). Модель делегирования: загрузчик сначала просит родителя, и только если тот не нашёл, грузит сам. Поэтому нельзя подменить java.lang.String своим классом. Класс в JVM идентифицируется парой «имя + загрузчик»: один и тот же класс, загруженный двумя загрузчиками, это два разных типа (отсюда ClassCastException: X cannot be cast to X в серверах приложений).
JIT
Байткод сначала интерпретируется, горячие методы компилирует C1 (быстро, с профилированием), самые горячие C2 (агрессивные оптимизации: инлайнинг, escape analysis, удаление проверок). Поэтому бенчмарк без прогрева врёт, а для микробенчмарков есть JMH.
Что спросят на собесе
- Какие области памяти есть в JVM, что лежит в стеке и в куче?
- Как GC понимает, что объект мусор? Что такое GC roots?
- Как устроены поколения и чем G1 отличается от ZGC?
- Как бывают утечки памяти в Java и как вы бы их искали?
- Как работает делегирование загрузчиков классов?