Экосистема Java богата качественными инструментами для разработчиков, и средства профилирования и диагностики - не исключение.
Существуют коммерческие профилировщики, есть встроенные инструменты профилирования в ведущих IDE. А если вам важна свобода (или цена является важным фактором), open source сообщество также готово предложить достойные альтернативы.
Несмотря на удобство графических инструментов, работа из командной строки остаётся актуальной в моей практике.
Сценарий, в котором доступность диагностического функционала из консоли становится критичной, выглядит следующим образом:
Случилась проблема
Специалист получает доступ к консоли сервера
Надо оперативно провести диагностику и идентифицировать причину проблемы, используя инструменты доступные в этой консоли
Иногда нет даже непосредственного доступа к консоли, и нужно помочь инженеру поддержки выполнить диагностику в режиме “созвона”.
JFR (JDK Flight Recorder) - один из ключевых инструментов для диагностики работы Java приложений, который можно эффективно использовать из командной строки. Приёмам работы с этим инструментом я хочу посвятить данную статью.
Разумеется, диагностика “здесь и сейчас” - не единственное применение консольным инструментам, они также могут быть полезны для автоматизации. Например, при тестировании производительности или для интеграции с мониторингом.
Как правило, сценарий работы с JFR состоит из 3 этапов:
Запуск сессии JFR
Получение “записи” - бинарного файла событий
Анализ
Часто для анализа используется JDK Mission Control или другой графический инструмент. Но в данной статье мы пройдём по всем трём этапам, используя командную строку.
Запуск сессии JFR
JFR является частью JDK, но пока не запущена сессия сбора данных, он никак не вмешивается в работу JVM.
При старте сессии JFR получает конфигурацию зондов (источников событий) и параметров буферизации событий в рамках этой сессии.
При этом несколько сессий могут работать в JVM совместно.
Можно выделить следующие варианты запуска сессии:
На старте JVM через параметры запуска
Из локальной консоли используя jcmd
Удалённо через JMX протокол
Программно из кода приложения JVM
Далее я подробно разберу первые два способа.
Третий предполагает предварительную настройку JMX, что не тривиально технически и требует внимания к безопасности (зато позволяет подключить графические инструменты).
Четвертый может быть интересен для глубокой интеграции JFR в вашу архитектуру мониторинга.
Запуск сессии JFR на старте JVM
Ключевое достоинство JFR - это то, что его можно начать использовать на запущенном процессе JVM, даже если никаких специальных параметров при запуске процесса указано не было.
Однако возможность собирать диагностику с момента старта JVM может быть крайне полезна в определённых сценариях.
Для запуска сессии JFR на старте JVM используется ключ -XX:StartFlightRecording, за которым после двоеточия идут параметры, разделённые запятой.
java -XX:StartFlightRecording:delay=10s,filename=myrecording.jfr,settings=profile …
Так, пример команды выше запустит сессию через 10 секунд после старта JVM c настройками сбора событий “profile”.
Настройка сбора событий - это имя xml файла, описывающего, какие события должны быть включены в сессию и параметры их сбора. Есть два стандартных конфигурационных файла, которые находяться в JAVA_HOME/lib/jfr.
default.jfc- включает набор событий с низкими накладными расходами.profile.jfc- включает существенно больше событий, размер результирующего файла растёт, а накладные расходы на их получение уже могут заметно влиять на приложение. Можно создать и собственную конфигурацию. К этому вопросу мы вернёмся чуть позже.
Исчерпывающую информацию по параметрам -XX:StartFlightRecording можно найти в статье “Управление Java Flight Recorder”.
Когда полезен запуск JFR на старте приложения?
Профилирование процесса запуска JVM и начальных этапов работы приложения (инициализации, прогрев кэшей и т.п.)
Старт “дежурной” сессии JFR
Профилирование процессов с ограниченным временем жизни
Запуск сессии JFR с помощью команды jcmd
jcmd - это универсальный инструмент JDK, позволяющий отправлять диагностические команды запущенному Java процессу, в том числе управлять работой JFR.
Ниже пример команды, позволяющей запустить JFR сессию с конфигурацией “profile” на 60 секунд.
jcmd 1234 JFR.start duration=60s filename=recording.jfr settings=profile 1234: Started recording 1. The result will be written to: /opt/demo/spring-petclinic/recording.jfr
Данная команда стартует сессию JFR, которая через 60 секунд автоматически завершится и дамп событий будет записан в файл с указанным именем.
Параметры, которые можно передать команде JFR.start, аналогичны параметрам опции -XX:StartFlightRecording, описанным выше.
Иногда вам нужно больше контроля над жизненным циклом JFR сессии. Для этого есть дополнительные команды.
Если не указывать параметр duration, сессия будет выполняться неопределенное время.
JFR.stop- позволяет остановить запущенную сессию.JFR.dump- сохранить события в файл без остановки сессии.JFR.check- посмотреть список активных сессий.
Ниже пример сессии профилирования через раздельный вызов команд JFR.start, JFR.dump и JFR.stop
Запуск сессии.
> jcmd 1234 JFR.start settings=profile 1234: Started recording 2. No limit specified, using maxsize=250MB as default. Use jcmd 1234 JFR.dump name=2 to copy recording data to file.
Дамп в файл.
> jcmd 1234 JFR.dump name=2 filename=recording2.jfr 1234: Dumped recording "2", 309.6 kB written to: /opt/demo/spring-petclinic/recording2.jfr
Останов сессии.
> jcmd 1234 JFR.stop name=2 1234: Stopped recording "2".
Так как в этом случае мы не указали продолжительность сессии, если её не остановить, то она будет накапливать данные неопределенно долго. Но не нужно переживать, что JFR может “съесть” весь диск, лимит по умолчанию 250 МБ далее новые события будут замещать старые.
Управление настройками сессии
Выше мы использовали в качестве настроек стандартные пресеты. Однако часто возникает желание “подкрутить” настройки под себя.
При использовании Mission Control редактор настроек, который позволяет модифицировать индивидуальные настройки событий, доступен в момент запуска сессии JFR.
Похожая возможность есть и при запуске через командную строку.
Допустим мы хотим использовать конфигурацию profile, но при этом убрать сбор событий для исключений.
Это можно сделать, переопределив параметры для некоторых событий на старте сессии.
jcmd 1234 JFR.start duration=10s settings=profile jdk.JavaExceptionThrow#enabled=false
В примере мы выключили запись события для исключений.
Помимо точного указания идентификатора события, можно использовать его имя. Кроме того, есть ещё несколько полезных сокращенией.
Вот несколько примеров настроек событий.
jcmd 1234 JFR.start gc=high method-profiling=high
Групповые настройки, такие как gc и method-profiling определены в jfc файле (в данном случае JAVA_HOME/lib/jfr/default.jfc).
jcmd 1234 JFR.start com.example.UserDefined#enabled=true \ com.example.UserDefined#threshold=100/s
Можно непосредственно указывать конкретные параметры конфигурации для событий используя идентификатор, это удобно для пользовательских событий.
jcmd 1234 JFR.start +.UserDefined#enabled=true +.UserDefined#threshold=100/s
Тоже самое можно написать короче, используя шаблон с символом “+”.
Создание файлов настроек
Прямое управление параметрами сбора событий через строку запуска удобно для небольших изменений конфигурации JFR. Если вы уже определились с набором настроек и хотите использовать его повторно, стоит создать собственный JFC-файл (файл конфигурации JFR).
Обычно, для этого я пользуюсь графическим редактором в Mission Control, но команда jfr, входящая в состав JDK (как и jcmd), позволяет также редактировать jfc файлы из командной строки.
Например, скопируем конфигурацию “profile” и изменим параметры сэмплирования методов.
jfr configure --input profile.jfc \ jdk.CPUTimeSample#enabled=true --output /tmp/custom.jfc
Запустить сессию с настройками из файла можно следующей командой.
jcmd 1234 JFR.start duration=10s settings=/tmp/custom.jfc
Теперь мы знаем, как запустить сессию JFR, используя нужную нам конфигурацию, и можно двигаться дальше - к работе с собранными данными.
Анализ данных JFR из командной строки
В результате манипуляций из предыдущей части мы тем или иным способом получаем файл с событиями JFR. Теперь нам нужно его проанализировать и прийти к каким-то выводам. Разумеется, для сложного анализа файл можно перенести на рабочую станцию и открыть в Mission Control, но это занимает время (и часто затруднено политиками безопасности).
Обработка файла событий прямо из командной строки, “не отходя от кассы”, даёт нам возможность быстро осуществить несложный анализ, чтобы сделать выводы о состоянии приложения.
Основной инструмент работы с файлами событий - команда jfr.
Эта команда позволяет распечатывать события в различных форматах.
> jfr print recording.jfr jdk.SafepointBegin { startTime = 04:00:38.849 (2025-10-31) duration = 0.00913 ms safepointId = 3409 totalThreadCount = 33 jniCriticalThreadCount = 0 eventThread = "VM Thread" (osThreadId = 45423) } jdk.ObjectAllocationSample { startTime = 04:00:38.850 (2025-10-31) objectClass = java.lang.String (classLoader = bootstrap) weight = 86.3 kB eventThread = "C1 CompilerThread0" (javaThreadId = 22) } ...
jfr print –json recording.jfr { "recording": { "events": [{ "type": "jdk.SafepointBegin", "values": { "startTime": "2025-10-31T04:00:38.849436842+03:00", "duration": "PT0.000009131S", "eventThread": { "osName": "VM Thread", "osThreadId": 45423, "javaName": null, "javaThreadId": 0, "group": null, "virtual": false }, "safepointId": 3409, "totalThreadCount": 33, "jniCriticalThreadCount": 0 } }, { ...
Один из доступных форматов - JSON, что позволяет передать данные JFR на вход традиционных инструментов анализа данных. Мне удобно пользоваться jq.
При диагностике, я первым делом смотрю на использование ресурсов ЦПУ.
Сколько ЦПУ потребляет процесс?
Сколько ЦПУ потребляет сборщик мусора?
Какие потоки потребляют больший процент ЦПУ?
С помощью jq, популярной утилиты для работы с JSON, дамп JFR позволяет ответить на все эти вопросы!
> jfr print --json --events ThreadCPULoad myrecording.jfr | jq ' [.recording.events[] | select(.type == "jdk.ThreadCPULoad") | { javaName: .values.eventThread.javaName, user: .values.user, system: .values.system} ] | group_by(.javaName) | map({ javaName: .[0].javaName, user: (map(.user) | add), system: (map(.system) | add) }) | sort_by(-.user)' [ { "javaName": "C2 CompilerThread1", "user": 1.4746960910000002, "system": 0.0628239843 }, { "javaName": "C2 CompilerThread0", "user": 0.27601727600000003, "system": 0.00119408049 }, { "javaName": "C1 CompilerThread0", "user": 0.11426830399999999, "system": 0.00334142412 }, { "javaName": "DestroyJavaVM", "user": 0.029225044, "system": 0.0012193053 }, { "javaName": "http-nio-8080-exec-8", "user": 0.0218696855, "system": 0.0008536230300000001 }, ...
До недавнего времени использование jq (или другого стороннего инструмента) было необходимо, поскольку команда jfr позволяла только распечатать события. Однако, начиная с JDK 21, стала доступна подкоманда jfr view, позволяющая строить типовые отчёты по файлам событий JFR.
Например, чтобы получить сводку об использовании ЦПУ процессом, можно использовать отчёт cpu-load.
> jfr view cpu-load recording.jfr CPU Load Statistics ------------------- JVM User (Minimum): 0.00% JVM User (Average): 0.08% JVM User (Maximum): 0.40% JVM System (Minimum): 0.00% JVM System (Average): 0.00% JVM System (Maximum): 0.07% Machine Total (Minimum): 0.00% Machine Total (Average): 0.15% Machine Total (Maximum): 0.65%
Для оценки накладных расходов сборщика мусора gc-cpu-time
> jfr view gc-cpu-time recording.jfr GC CPU Time ----------- GC User Time: 1.94 s GC System Time: 150 ms GC Wall Clock Time: 920 ms Total Time: 56.0 s GC Count: 235
Сводка использования CPU по потокам: thread-cpu-load
jfr view thread-cpu-load recording.jfr Thread CPU Load Thread System User ------------------------------------------------------------------ ------ ----- C2 CompilerThread2 0.18% 1.27% C2 CompilerThread1 0.17% 1.20% http-nio-8080-exec-7 0.01% 0.25% http-nio-8080-exec-3 0.00% 0.24% http-nio-8080-exec-12 0.00% 0.22% http-nio-8080-exec-10 0.00% 0.22% http-nio-8080-exec-14 0.01% 0.22% http-nio-8080-exec-2 0.01% 0.22% http-nio-8080-exec-15 0.02% 0.20% http-nio-8080-exec-1 0.01% 0.20% http-nio-8080-exec-11 0.02% 0.20% C2 CompilerThread0 0.00% 0.17% http-nio-8080-exec-4 0.00% 0.16% http-nio-8080-exec-8 0.00% 0.16% http-nio-8080-Poller 0.01% 0.03% ...
В примере выше стоит помнить, что 100% считается от всех ядер в системе. Например, если в системе 8 ядер, максимальное потребление, которого может достичь один поток - 100% / 8 = 12.5%.
Если вы не уверены сколько ядер в системе, эта информация также доступна в дампесобытий JFR.
> jfr view system-information recording.jfr System Information ------------------ Total Physical Memory Size: 7.4 GB OS Version: DISTRIB_ID=Ubuntu DISTRIB_RELEASE=24.04 DISTRIB_CODENAME=noble DIST RIB_DESCRIPTION="Ubuntu 24.04.1 LTS" uname: Linux 5.15.146.1-micros oft-standard-WSL2 #1 SMP Thu Jan 11 04:09:03 UTC 2024 x86_64 libc: glibc 2.39 NPTL 2.39 Virtualization: Hyper-V virtualization CPU Type: Intel (null) (HT) SSE SSE2 SSE3 SSSE3 SSE4.1 SSE4.2 Core Intel64 Number of Cores: 8 Number of Hardware Threads: 14 Number of Sockets: 1 CPU Description: Brand: Intel(R) Core(TM) Ultra 5 125U, Vendor: GenuineIntel Fa mily: <unknown> (0x6), Model: <unknown> (0xaa), Stepping: 0x4 Ext. family: 0x0, Ext. model: 0xa, Type: 0x0, Signature: 0x000 a06a4 Features: ebx: 0x09100800, ecx: 0xfeda3203, edx: 0x1f8bf bff Ext. features: eax: 0x00000000, ebx: 0x00000000, ecx: 0x00 000121, edx: 0x2c100800 Supports: On-Chip FPU, Virtual Mode Ex tensions, Debugging Extensions, Page Size Extensions, Time Sta mp Counter, Model Specific Registers, Physical Address Extensi on, Machine Check Exceptions, CMPXCHG8B Instruction, On-Chip A PIC, Fast System Call, Memory Type Range Registers, Page Globa l Enable, Machine Check Architecture, Conditional Mov Instruct ion, Page Attribute Table, 36-bit Page Size Extension, CLFLUSH Instruction, Intel Architecture MMX Technology, Fast Float Po int Save and Restore, Streaming SIMD extensions, Streaming SIM D extensions 2, Self-Snoop, Hyper Threading, Streaming SIMD Ex tensions 3, PCLMULQDQ, Supplemental Streaming SIMD Extensions 3, Fused Multiply-Add, CMPXCHG16B, Process-context identifiers , Streaming SIMD extensions 4.1, Streaming SIMD extensions 4.2 , MOVBE, Popcount instruction, AESNI, XSAVE, OSXSAVE, AVX, F16 C, LAHF/SAHF instruction support, Advanced Bit Manipulations: LZCNT, SYSCALL/SYSRET, Execute Disable Bit, RDTSCP, Intel 64 A rchitecture, Invariant TSC
jfr view включает широкий набор готовых отчётов, позволяющих быстро проверять гипотезы о возможных причинах проблем.
Ниже список очётов, доступных в JDK 25
Java virtual machine views: blocked-by-system-gc gc-concurrent-phases jvm-information class-modifications gc-configuration longest-compilations compiler-configuration gc-cpu-time native-memory-committed compiler-phases gc-parallel-phases native-memory-reserved compiler-statistics gc-pause-phases safepoints deoptimizations-by-reason gc-pauses tlabs deoptimizations-by-site gc-references vm-operations gc heap-configuration Environment views: active-recordings cpu-load native-library-failures active-settings cpu-load-samples network-utilization container-configuration cpu-tsc recording container-cpu-throttling environment-variables system-information container-cpu-usage events-by-count system-processes container-io-usage events-by-name system-properties container-memory-usage jvm-flags Application views: allocation-by-class exception-by-site method-timing allocation-by-site exception-by-type modules allocation-by-thread exception-count monitor-inflation class-loaders file-reads-by-path native-methods contention-by-address file-writes-by-path object-statistics contention-by-class finalizers pinned-threads contention-by-site hot-methods socket-reads-by-host contention-by-thread latencies-by-type socket-writes-by-host cpu-time-hot-methods longest-class-loading thread-allocation cpu-time-statistics memory-leaks-by-class thread-count deprecated-methods-for-removal memory-leaks-by-site thread-cpu-load exception-by-message method-calls thread-start
Помимо перечисленных отчётов можно указать идентификатор JFR события, чтобы распечатать события в табличной форме.
> jfr view --width 80 jdk.ThreadStart recording.jfr Java Thread Start Time Event Thread Stack Trace New Java Thread Parent Java Th... -------- ----------------- ----------------- ----------------- ----------------- 04:48:00 http-nio-8080-... org.apache.tom... http-nio-8080-... http-nio-8080-... 04:48:03 C2 CompilerThr... N/A C2 CompilerThr... C2 CompilerThr... 04:48:05 C2 CompilerThr... N/A C2 CompilerThr... C1 CompilerThr... 04:48:05 C2 CompilerThr... N/A C2 CompilerThr... C1 CompilerThr... 04:48:06 C2 CompilerThr... N/A C2 CompilerThr... C1 CompilerThr... 04:48:06 C2 CompilerThr... N/A C2 CompilerThr... C1 CompilerThr... 04:48:06 HikariPool-1:c... java.lang.Syst... HikariPool-1:c... http-nio-8080-...
Приведу ещё несколько примеров отчётов команды jfr view.
thread-allocation - позволяет получить топ потоков по интенсивности аллокации памяти
> jfr view thread-allocation recording.jfr Thread Allocation Statistics Thread Allocated Percentage ---------------------------------------------------------- --------- ---------- http-nio-8080-exec-12 9.6 GB 10.30% http-nio-8080-exec-7 9.4 GB 10.08% http-nio-8080-exec-3 9.4 GB 10.05% http-nio-8080-exec-4 9.3 GB 9.97% http-nio-8080-exec-10 9.3 GB 9.97% http-nio-8080-exec-1 9.3 GB 9.96% http-nio-8080-exec-11 9.0 GB 9.62% http-nio-8080-exec-2 8.9 GB 9.53% http-nio-8080-exec-8 8.9 GB 9.49% http-nio-8080-exec-14 8.8 GB 9.40% http-nio-8080-exec-15 1.3 GB 1.43% Notification Thread 89.2 MB 0.09%
Также можно посмотреть точки интенсивной аллокации
> jfr view --width 80 allocation-by-site recording.jfr Allocation by Site Method Allocation Pressure ----------------------------------------------------------- ------------------- org.apache.tomcat.util.http.parser.Host.parse(MessageBytes) 76.72% org.springframework.web.util.pattern.PathPattern.matches... 8.50% java.util.Arrays.copyOfRange(byte[], int, int) 1.53% jdk.internal.misc.Unsafe.allocateUninitializedArray(...) 1.22% sun.util.calendar.Gregorian.newCalendarDate(TimeZone) 0.61% java.util.HashMap.newNode(...) 0.52% org.hibernate.engine.internal.StatefulPersistenceContext... 0.51% org.springframework.core.ResolvableType.forType(...) 0.45% java.util.TimeZone.clone() 0.43% java.lang.invoke.DirectMethodHandle.allocateInstance(...) 0.36% org.thymeleaf.engine.Model.<init>(...) 0.35% java.lang.StringLatin1.newString(byte[], int, int) 0.33% java.util.Arrays.copyOf(Object[], int) 0.31% sun.util.calendar.AbstractCalendar.getCalendarDate(...) 0.28% org.springframework.boot.loader.net.protocol.jar.JarUrlC... 0.28% org.hibernate.engine.internal.EntityEntryContext.addEnti... 0.27% jdk.internal.loader.URLClassPath$Loader.findResource(...) 0.26%
exception-by-type - позволяет посмотреть статистику исключений в приложении. Иногда они обрабатываются в логике и никогда не попадают в логи, но при этом могут быть индикатором проблемы.
> jfr view exception-by-type recording.jfr Exceptions by Type Class Count --------------------------------- ------ java.io.EOFException 189 java.lang.ClassNotFoundException 2
Несмотря на богатство отчётов, стоит признать, что табличные представления jfr view достаточно поверхностны, у них отсутствуют возможности фильтрации (например, часто хочется отсечь нерелевантные потоки) и полноценным инструментом анализа эту команду назвать сложно.
Тем не менее, возможность быстро получить сводки по дампу JVM крайне удобна на практике и сокращает число “jq заклинаний”, которые надо держать под рукой.
Получение сводки событий JFR прямо с JVM
В начале статьи я писал про 3 шага работы с JFR
Сбор событий
Запись дампа
Анализ
Но это ещё не все. Начиная с JDK 15 события JFR можно получать с минимальными задержками через потоковый API. А в JDK 21, jcmd пополнилась командой JFR.view, полностью аналогичной консольной команде jfr view, которая строит отчёты по событиям собранным в рамках активных сессий без необходимости записи их в файл.
Конечно, JFR.view не подходит для глубокого анализа, однако она позволяет быстро оценить состояния JVM.
Также иногда полезно проконтролировать корректность запуска сессии. Например, проверить, какие события стали накапливаться.
> jcmd 1234 JFR.view events-by-count 1234: Event Types by Count (Experimental) Event Type Count --------------------------------- ----- Native Sample 1,831 Boolean Flag 494 Recording Setting 379 Long Flag 137 Java Thread Park 125 Unsigned Long Flag 92 Java Thread Statistics 51 Class Loading Statistics 51 CPU Load 51 Compiler Statistics 51 Exception Statistics 51 Resident Set Size 51 Thread CPU Load 50 ... TLAB Configuration 1 GC Configuration 1 Timespan: 04:51:47 - 05:01:47
Заключение
В своей работе я всегда ценил возможность провести диагностику работы JVM из командной строки. Даже написал для этого собственный минималистичный профайлер - SJK.
JDK Flight Recorder начинал с парадигмы: собрать (инструментация JVM) -> доставить (самодостаточный файл) -> проанализировать (на рабочем месте эксперта).
На ранних этапах своего развития он не очень подходил для сценариев работы из консоли.
С тех пор он прошёл серьёзный путь: jcmd позволила управлять работой JFR из командной строки, a команда jfr - проводить базовый анализ и экспортировать данные в JSON. Также нужно отметить богатство сигналов/событий собираемых JFR, число которых растёт с каждым релизом.
Да, опыт работы с JFR из командной строки не идеален, и многие аспекты можно улучшить. Но в целом это рабочий инструмент решающий реальные задачи.
Надеюсь, данная статья помогла вам лучше узнать его возможности.
Больше экспертных статей, событий и новостей из мира Java можно найти в тг-канале Axiom JDK.