Перейти к основному содержимому

JVM в проде — jcmd, дамп памяти и JFR

Разработчику Архитектору

JVM в проде — jcmd, дамп памяти и JFR

Теория GC и потоков — в JVM, память и потоки. Здесь — практика — какие команды ввести, когда приложение тормозит, "ест" память или упало с OutOfMemoryError.

Инструменты входят в JDK (каталог bin рядом с java), не в урезанную JRE — jcmd, jmap, jfr.


Контекст перед началом

Эта тема часто кажется "слишком низкоуровневой", но на практике она полезна даже junior-разработчику. Когда сервис тормозит, команда ждёт не теорию, а набор артефактов — thread dump, heap dump, JFR и короткое описание симптомов.

Простое правило: сначала собрать факты, потом перезапускать сервис.


Словарь перед командами

ТерминОбъяснение
JVMПроцесс, в котором крутится ваш .jar (Spring Boot, Tomcat внутри)
PIDНомер процесса в ОС; подставляете вместо <pid> в командах
Heap (куча)Память для объектов (new, коллекции); OOM Java heap space — про неё
MetaspaceПамять под метаданные классов; отдельный OOM Metaspace
Heap dumpСнимок всех объектов в куче в файл .hprof для анализа
Thread dumpСнимок стеков всех потоков в момент времени — ищут deadlock и "горячий" код
JFRJava Flight Recorder — короткая запись профиля: CPU, GC, аллокации
GCСборщик мусора освобождает объекты без ссылок

Когда что делать (дерево решений):

Приложение "висит" или CPU 100% → Thread.print (jcmd)
OutOfMemoryError / память растёт → heap dump + Eclipse MAT / IDEA
Нужно "где тормозит метод" → JFR 60 сек + JDK Mission Control

Когда открывать эту статью

СимптомПервый шаг
"Висит", CPU 100%jcmd <pid> Thread.print
OutOfMemoryError: Java heap spaceheap dump + анализ
Нужно понять, куда ушло времякороткая запись JFR

1. Найти PID процесса

Linux / macOS:

jcmd
# или
ps aux | grep java

Windows (PowerShell):

jcmd
# или диспетчер задач → подробности → java.exe

Запомните число PID — подставляйте вместо <pid> ниже. Если процессов Java несколько, смотрите колонку с main-классом или аргументами (com.example.demo.DemoApplication).

Проверка JDK: java -version и jcmd -h должны работать из той же установки, которой запущено приложение.


2. Снимок потоков (зависания, высокий CPU)

jcmd <pid> Thread.print > threads.txt

Откройте threads.txt: ищите повторяющийся стек в состоянии RUNNABLE — часто там цикл или блокировка.

Разбор файла threads.txt:

Что видитеКак читать
"main" #1 prio=5 os_prio=0 tid=...Имя потока и номер
java.lang.Thread.State: RUNNABLEПоток выполняется или ждёт CPU
BLOCKED / WAITINGЖдёт lock или wait()
at com.example.service.Foo.bar(Foo.java:42)Стек: сверху вниз — от текущего кадра к main
Один и тот же at ... десятки раз в RUNNABLEЧастый признак бесконечного цикла или горячего метода

Deadlock: в конце дампа JVM иногда печатает Found one Java-level deadlock — два потока держат lock друг друга.


3. Heap dump (утечки памяти, OOM)

jcmd <pid> GC.heap_dump heap.hprof

Альтернатива (устаревающая, но встречается):

jmap -dump:live,format=b,file=heap.hprof <pid>

Файл heap.hprof откройте в Eclipse MAT, VisualVM или IntelliJ IDEA (Analyze Heap Dump).

На что смотреть в анализаторе:

  • Dominator tree — кто держит больше всего памяти;
  • Leak Suspects — эвристики утечек;
  • сравнение двух дампов "до/после" нагрузки.
OOM в логах

Запустите JVM с -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./dumps — дамп появится автоматически при падении.


4. Краткая запись JFR (профилирование)

Java Flight Recorder — встроенный профайлер Oracle JDK / Temurin 11+.

Запись 60 секунд:

jcmd <pid> JFR.start name=debug settings=profile duration=60s filename=recording.jfr
# подождать минуту
jcmd <pid> JFR.dump name=debug filename=recording.jfr
jcmd <pid> JFR.stop name=debug

Откройте recording.jfr в JDK Mission Control (JMC) или в IDEA (Analyze JFR Snapshot).

Разбор: в JMC смотрите Method Profiling, GC, Memory — где CPU и аллокации.


5. Минимальный чек-лист инцидента

  1. Зафиксировать время и версию (java -version).
  2. jcmd <pid> VM.flags — флаги JVM.
  3. jcmd <pid> GC.heap_info — размер кучи.
  4. Thread.print при подозрении на deadlock/CPU.
  5. Heap dump или JFR — по типу проблемы.
  6. Перезапуск только после сбора артефактов (если политика позволяет).

Подробнее о памяти и GC — JVM, память и потоки. Полная шпаргалка по флагам запуска (-D, -X, -XX) — Справочник по Java, §24.


Пример — учебная утечка

Код ITЗагрузка примера кода…

Запуск с дампом при OOM:

java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=. LeakDemo

В MAT список byte[] и статическая LEAK будут доминировать — хорошая тренировка чтения дампа.


Частые ошибки

ОшибкаЧто делать
jcmd не найденВ PATH должен быть каталог bin JDK, не JRE
Отказ в доступе к PIDПрава пользователя / sudo (осторожно в проде)
Огромный .hprofДамп на диск с местом; live уменьшает размер
Путать heap и metaspaceOOM бывает разный — читайте текст исключения

Что попробовать

  1. Снять Thread.print для своего Spring Boot под нагрузкой ab или curl в цикле.
  2. Включить HeapDumpOnOutOfMemoryError в dev-профиле.
  3. Вернуться к JVM, память и потоки и сопоставить GC-логи с JFR.

Дальше

JVM, память и потоки · Отладка в IDEA · Testcontainers


Быстрый runbook для дежурства

Ниже шаблон, который удобно хранить в internal wiki и использовать прямо во время инцидента:

  1. Зафиксировать время инцидента и PID.
  2. Снять Thread.print два раза с интервалом 10-20 секунд.
  3. Снять GC.heap_info и при необходимости heap_dump.
  4. Запустить JFR на 60-120 секунд под реальной нагрузкой.
  5. Приложить артефакты к тикету и только потом перезапустить процесс.

Если пропустить шаги 2-4, корневая причина часто теряется сразу после рестарта.


Как интерпретировать результат без перегруза

Признаки CPU-проблемы

  • несколько потоков в RUNNABLE;
  • похожие вершины стека в горячих потоках;
  • в JFR высокий self-time у небольшого числа методов.

Признаки memory-проблемы

  • рост heap после Full GC без возврата к прежнему уровню;
  • крупные доминаторы в MAT (byte[], HashMap, кэши);
  • повторяющийся OutOfMemoryError под одинаковой нагрузкой.

Признаки lock contention проблемы

  • много потоков в BLOCKED на одном и том же мониторе;
  • длинные цепочки ожиданий в ReentrantLock$NonfairSync или похожих кадрах;
  • в JFR растут события monitor enter и lock stall.

Runbook при гонке или deadlock

  1. Снять Thread.print минимум два раза с интервалом 10-20 секунд.
  2. Сравнить верхние кадры стеков. Одинаковые RUNNABLE стеки обычно указывают на горячий цикл, массовый BLOCKED часто указывает на lock contention.
  3. Проверить конец дампа на Found one Java-level deadlock.
  4. Запустить JFR на 60 секунд и открыть вкладки Threads и Locks.
  5. Зафиксировать список проблемных классов/методов и только после этого менять таймауты или размер пулов.

Эта последовательность помогает не перепутать CPU saturation, deadlock и обычное ожидание внешнего I/O.

Полезные ссылки для расшифровки терминов:


Частый сценарий в Spring Boot

Сервис получает всплеск трафика, кэш не ограничен, память растёт, latency ухудшается. В таком случае действуют так:

  1. jcmd <pid> GC.heap_info — зафиксировать текущий размер heap.
  2. jcmd <pid> JFR.start ... duration=60s — снять профиль.
  3. jcmd <pid> GC.heap_dump heap.hprof — сделать дамп.
  4. Проверить кэш-стратегию и TTL в коде, затем повторить нагрузочный тест.

Связанные материалы: