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 и "горячий" код |
| JFR | Java 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 space | heap 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 — эвристики утечек;
- сравнение двух дампов "до/после" нагрузки.
Запустите 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. Минимальный чек-лист инцидента
- Зафиксировать время и версию (
java -version). jcmd <pid> VM.flags— флаги JVM.jcmd <pid> GC.heap_info— размер кучи.Thread.printпри подозрении на deadlock/CPU.- Heap dump или JFR — по типу проблемы.
- Перезапуск только после сбора артефактов (если политика позволяет).
Подробнее о памяти и 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 и metaspace | OOM бывает разный — читайте текст исключения |
Что попробовать
- Снять
Thread.printдля своего Spring Boot под нагрузкойabилиcurlв цикле. - Включить
HeapDumpOnOutOfMemoryErrorв dev-профиле. - Вернуться к JVM, память и потоки и сопоставить GC-логи с JFR.
Дальше
JVM, память и потоки · Отладка в IDEA · Testcontainers
Быстрый runbook для дежурства
Ниже шаблон, который удобно хранить в internal wiki и использовать прямо во время инцидента:
- Зафиксировать время инцидента и PID.
- Снять
Thread.printдва раза с интервалом 10-20 секунд. - Снять
GC.heap_infoи при необходимостиheap_dump. - Запустить JFR на 60-120 секунд под реальной нагрузкой.
- Приложить артефакты к тикету и только потом перезапустить процесс.
Если пропустить шаги 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
- Снять
Thread.printминимум два раза с интервалом 10-20 секунд. - Сравнить верхние кадры стеков. Одинаковые
RUNNABLEстеки обычно указывают на горячий цикл, массовыйBLOCKEDчасто указывает на lock contention. - Проверить конец дампа на
Found one Java-level deadlock. - Запустить JFR на 60 секунд и открыть вкладки Threads и Locks.
- Зафиксировать список проблемных классов/методов и только после этого менять таймауты или размер пулов.
Эта последовательность помогает не перепутать CPU saturation, deadlock и обычное ожидание внешнего I/O.
Полезные ссылки для расшифровки терминов:
Частый сценарий в Spring Boot
Сервис получает всплеск трафика, кэш не ограничен, память растёт, latency ухудшается. В таком случае действуют так:
jcmd <pid> GC.heap_info— зафиксировать текущий размер heap.jcmd <pid> JFR.start ... duration=60s— снять профиль.jcmd <pid> GC.heap_dump heap.hprof— сделать дамп.- Проверить кэш-стратегию и TTL в коде, затем повторить нагрузочный тест.
Связанные материалы: