Урок по треку Java 8 мин

JVM: память, сборка мусора, загрузка классов

Heap и стек, Metaspace, сборщики G1 и ZGC, загрузчики классов, JIT и утечки памяти.

Вопросы про 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 memoryByteBuffer.allocateDirect, буферы NettyOutOfMemoryError: 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 и как вы бы их искали?
  • Как работает делегирование загрузчиков классов?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.