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

Диагностика работающей JVM

· 11 мин. чтения

1. Обзор

Виртуальная машина Java (JVM) — это виртуальная машина, которая позволяет компьютеру запускать программы Java. В этой статье мы увидим, как легко диагностировать работающую JVM.

У нас есть много инструментов, доступных в самом JDK, которые можно использовать для различных действий по разработке, мониторингу и устранению неполадок. Давайте взглянем на jcmd , который довольно прост в использовании и может предоставить различную информацию о работающей JVM. Кроме того, начиная с версии JDK 7, jcmd является рекомендуемым инструментом для расширенной диагностики JVM без снижения производительности или с минимальными потерями производительности.

2. Что такое jcmd ?

Это утилита, которая отправляет запросы диагностических команд на работающую JVM. Однако он должен использоваться на той же машине, на которой работает JVM. Дополнительные подробности доступны в его документации .

Давайте посмотрим, как мы можем использовать эту утилиту с примером приложения Java, работающего на сервере.

3. Как использовать jcmd ?

Давайте создадим быстрое демонстрационное веб-приложение, используя Spring Initializr с JDK11 . Теперь давайте запустим сервер и продиагностируем его с помощью jcmd .

3.1. Получение PID

Мы знаем, что у каждого процесса есть связанный идентификатор процесса, известный как PID . Следовательно, чтобы получить связанный PID для нашего приложения, мы можем использовать jcmd , который перечислит все применимые процессы Java, как показано ниже:

root@c6b47b129071:/# jcmd
65 jdk.jcmd/sun.tools.jcmd.JCmd
18 /home/pgm/demo-0.0.1-SNAPSHOT.jar
root@c6b47b129071:/#

Здесь мы видим, что PID нашего работающего приложения равен 18.

3.2. Получить список возможного использования jcmd

Давайте выясним возможные параметры, доступные с помощью команды jcmd PID help для начала:

root@c6b47b129071:/# jcmd 18 help
18:
The following commands are available:
Compiler.CodeHeap_Analytics
Compiler.codecache
Compiler.codelist
Compiler.directives_add
Compiler.directives_clear
Compiler.directives_print
Compiler.directives_remove
Compiler.queue
GC.class_histogram
GC.class_stats
GC.finalizer_info
GC.heap_dump
GC.heap_info
GC.run
GC.run_finalization
JFR.check
JFR.configure
JFR.dump
JFR.start
JFR.stop
JVMTI.agent_load
JVMTI.data_dump
ManagementAgent.start
ManagementAgent.start_local
ManagementAgent.status
ManagementAgent.stop
Thread.print
VM.class_hierarchy
VM.classloader_stats
VM.classloaders
VM.command_line
VM.dynlibs
VM.flags
VM.info
VM.log
VM.metaspace
VM.native_memory
VM.print_touched_methods
VM.set_flag
VM.stringtable
VM.symboltable
VM.system_properties
VM.systemdictionary
VM.uptime
VM.version
help

Доступные диагностические команды могут различаться в разных версиях HotSpot VM.

4. Команды jcmd

Давайте рассмотрим некоторые из наиболее полезных параметров команды jcmd для диагностики нашей работающей JVM.

4.1. Версия ВМ

Это нужно для получения основных сведений о JVM, как показано ниже:

root@c6b47b129071:/# jcmd 18 VM.version
18:
OpenJDK 64-Bit Server VM version 11.0.11+9-Ubuntu-0ubuntu2.20.04
JDK 11.0.11
root@c6b47b129071:/#

Здесь мы видим, что мы используем OpenJDK 11 для нашего примера приложения.

4.2. VM.system_properties

Это напечатает все системные свойства, установленные для нашей виртуальной машины. Может отображаться несколько сотен строк информации:

root@c6b47b129071:/# jcmd 18 VM.system_properties
18:
#Thu Jul 22 10:56:13 IST 2021
awt.toolkit=sun.awt.X11.XToolkit
java.specification.version=11
sun.cpu.isalist=
sun.jnu.encoding=ANSI_X3.4-1968
java.class.path=/home/pgm/demo-0.0.1-SNAPSHOT.jar
java.vm.vendor=Ubuntu
sun.arch.data.model=64
catalina.useNaming=false
java.vendor.url=https\://ubuntu.com/
user.timezone=Asia/Kolkata
java.vm.specification.version=11
...

4.3. VM.flags

Для нашего примера приложения это напечатает все используемые аргументы VM, либо заданные нами, либо используемые по умолчанию JVM. Здесь мы можем заметить различные аргументы виртуальной машины по умолчанию, как показано ниже:

root@c6b47b129071:/# jcmd 18 VM.flags            
18:
-XX:CICompilerCount=3 -XX:CompressedClassSpaceSize=260046848 -XX:ConcGCThreads=1 -XX:G1ConcRefinementThreads=4 -XX:G1HeapRegionSize=1048576 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=536870912 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=536870912 -XX:MaxMetaspaceSize=268435456 -XX:MaxNewSize=321912832 -XX:MinHeapDeltaBytes=1048576 -XX:NonNMethodCodeHeapSize=5830732 -XX:NonProfiledCodeHeapSize=122913754 -XX:ProfiledCodeHeapSize=122913754 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:ThreadStackSize=256 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC
root@c6b47b129071:/#

Точно так же другие команды, такие как VM.command_line , VM.uptime , VM.dynlibs, также предоставляют другие основные и полезные сведения о различных других используемых свойствах.

Все вышеперечисленные команды в основном предназначены для получения различных сведений , связанных с JVM . Теперь давайте рассмотрим еще несколько команд, которые могут помочь в устранении некоторых неполадок, связанных с JVM.

4.4. Thread.print

Эта команда предназначена для получения мгновенного дампа потока. Следовательно, он будет печатать трассировку стека всех запущенных потоков. Ниже приведен способ его использования, который может дать длинный вывод в зависимости от количества используемых потоков:

root@c6b47b129071:/# jcmd 18 Thread.print
18:
2021-07-22 10:58:08
Full thread dump OpenJDK 64-Bit Server VM (11.0.11+9-Ubuntu-0ubuntu2.20.04 mixed mode, sharing):

Threads class SMR info:
_java_thread_list=0x00007f21cc0028d0, length=25, elements={
0x00007f2210244800, 0x00007f2210246800, 0x00007f221024b800, 0x00007f221024d800,
0x00007f221024f800, 0x00007f2210251800, 0x00007f2210253800, 0x00007f22102ae800,
0x00007f22114ef000, 0x00007f21a44ce000, 0x00007f22114e3800, 0x00007f221159d000,
0x00007f22113ce800, 0x00007f2210e78800, 0x00007f2210e7a000, 0x00007f2210f20800,
0x00007f2210f22800, 0x00007f2210f24800, 0x00007f2211065000, 0x00007f2211067000,
0x00007f2211069000, 0x00007f22110d7800, 0x00007f221122f800, 0x00007f2210016000,
0x00007f21cc001000
}

"Reference Handler" #2 daemon prio=10 os_prio=0 cpu=2.32ms elapsed=874.34s tid=0x00007f2210244800 nid=0x1a waiting on condition [0x00007f221452a000]
java.lang.Thread.State: RUNNABLE
at java.lang.ref.Reference.waitForReferencePendingList(java.base@11.0.11/Native Method)
at java.lang.ref.Reference.processPendingReferences(java.base@11.0.11/Reference.java:241)
at java.lang.ref.Reference$ReferenceHandler.run(java.base@11.0.11/Reference.java:213)

"Finalizer" #3 daemon prio=8 os_prio=0 cpu=0.32ms elapsed=874.34s tid=0x00007f2210246800 nid=0x1b in Object.wait() [0x00007f22144e9000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(java.base@11.0.11/Native Method)
- waiting on <0x00000000f7330898> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(java.base@11.0.11/ReferenceQueue.java:155)
- waiting to re-lock in wait() <0x00000000f7330898> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(java.base@11.0.11/ReferenceQueue.java:176)
at java.lang.ref.Finalizer$FinalizerThread.run(java.base@11.0.11/Finalizer.java:170)

"Signal Dispatcher" #4 daemon prio=9 os_prio=0 cpu=0.40ms elapsed=874.33s tid=0x00007f221024b800 nid=0x1c runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

Подробное обсуждение захвата дампа потока с помощью других опций можно найти здесь .

4.5. GC.class_histogram

Давайте воспользуемся другой командой jcmd , которая предоставит важную информацию об использовании кучи. Кроме того, в нем будут перечислены все классы (внешние или специфичные для приложения) со многими экземплярами. Опять же, список может состоять из сотен строк в зависимости от количества используемых классов:

root@c6b47b129071:/# jcmd 18 GC.class_histogram
18:
num #instances #bytes class name (module)
-------------------------------------------------------
1: 41457 2466648 [B (java.base@11.0.11)
2: 38656 927744 java.lang.String (java.base@11.0.11)
3: 6489 769520 java.lang.Class (java.base@11.0.11)
4: 21497 687904 java.util.concurrent.ConcurrentHashMap$Node (java.base@11.0.11)
5: 6570 578160 java.lang.reflect.Method (java.base@11.0.11)
6: 6384 360688 [Ljava.lang.Object; (java.base@11.0.11)
7: 9668 309376 java.util.HashMap$Node (java.base@11.0.11)
8: 7101 284040 java.util.LinkedHashMap$Entry (java.base@11.0.11)
9: 3033 283008 [Ljava.util.HashMap$Node; (java.base@11.0.11)
10: 2919 257000 [I (java.base@11.0.11)
11: 212 236096 [Ljava.util.concurrent.ConcurrentHashMap$Node; (java.base@11.0.11)

Однако, если это не дает четкой картины, мы можем получить дамп кучи. Давайте посмотрим на это дальше.

4.6. G C.heap_dump

Эта команда даст мгновенный дамп кучи JVM . Поэтому мы можем извлечь дамп кучи в файл для последующего анализа, как показано ниже:

root@c6b47b129071:/# jcmd 18 GC.heap_dump ./demo_heap_dump
18:
Heap dump file created
root@c6b47b129071:/#

Здесь demo_heap_dump — это имя файла дампа кучи. Кроме того, он будет создан в том же месте, где находится наша банка приложений.

4.7. Параметры команды JFR

В предыдущей статье мы обсуждали мониторинг приложений Java с помощью JFR и JMC . Теперь давайте рассмотрим команды jcmd , которые мы можем использовать для анализа проблем с производительностью нашего приложения.

JFR (или Java Flight Recorder) — это инфраструктура профилирования и сбора событий, встроенная в JDK. JFR позволяет нам собирать подробную низкоуровневую информацию о том, как ведут себя приложения JVM и Java. Кроме того, мы можем использовать JMC для визуализации данных, собранных JFR . Следовательно, JFR и JMC вместе создают полную цепочку инструментов для непрерывного сбора низкоуровневой и подробной информации о времени выполнения.

Хотя использование JMC выходит за рамки этой статьи, мы увидим, как мы можем создать файл JFR с помощью jcmd . JFR — это коммерческая функция. Следовательно, по умолчанию он отключен. Однако это можно включить с помощью jcmd PID VM.unlock_commercial_features .

Однако для нашей статьи мы использовали OpenJDK . Следовательно , JFR включен для нас. Теперь давайте сгенерируем файл JFR с помощью команды jcmd , как показано ниже:

root@c6b47b129071:/# jcmd 18 JFR.start name=demo_recording settings=profile delay=10s duration=20s filename=./demorecording.jfr
18:
Recording 1 scheduled to start in 10 s. The result will be written to:

/demorecording.jfr
root@c6b47b129071:/# jcmd 18 JFR.check
18:
Recording 1: name=demo_recording duration=20s (delayed)
root@c6b47b129071:/# jcmd 18 JFR.check
18:
Recording 1: name=demo_recording duration=20s (running)
root@c6b47b129071:/# jcmd 18 JFR.check
18:
Recording 1: name=demo_recording duration=20s (stopped)

Мы создали пример файла записи JFR с именем demorecording.jfr в том же месте, где находится наше приложение jar. Кроме того, эта запись длится 20 секунд и настроена в соответствии с требованиями.

Кроме того, мы можем проверить состояние записи JFR с помощью команды JFR.check . И мы можем мгновенно остановить и отменить запись с помощью команды JFR.stop . С другой стороны, команду JFR.dump можно использовать для мгновенной остановки и создания дампа записи.

4.8. VM.native_memory

Это одна из лучших команд, которая может предоставить много полезных сведений о куче и не куче памяти в JVM. Следовательно, это можно использовать для настройки использования памяти и обнаружения любых утечек памяти. Как мы знаем, память JVM можно в широком смысле разделить на память с кучей и без кучи. И чтобы получить подробную информацию о полном использовании памяти JVM, мы можем использовать эту утилиту. Кроме того, это может быть полезно при определении размера памяти для приложения на основе контейнера.

Чтобы использовать эту функцию, нам нужно перезапустить наше приложение с дополнительным аргументом VM, т.е. – XX:NativeMemoryTracking=summary или -XX:NativeMemoryTracking=detail . Обратите внимание, что включение NMT вызывает снижение производительности на 5-10%.

Это даст нам новый PID для диагностики:

root@c6b47b129071:/# jcmd 19 VM.native_memory
19:

Native Memory Tracking:

Total: reserved=1159598KB, committed=657786KB
- Java Heap (reserved=524288KB, committed=524288KB)
(mmap: reserved=524288KB, committed=524288KB)

- Class (reserved=279652KB, committed=29460KB)
(classes #6425)
( instance classes #5960, array classes #465)
(malloc=1124KB #15883)
(mmap: reserved=278528KB, committed=28336KB)
( Metadata: )
( reserved=24576KB, committed=24496KB)
( used=23824KB)
( free=672KB)
( waste=0KB =0.00%)
( Class space:)
( reserved=253952KB, committed=3840KB)
( used=3370KB)
( free=470KB)
( waste=0KB =0.00%)

- Thread (reserved=18439KB, committed=2699KB)
(thread #35)
(stack: reserved=18276KB, committed=2536KB)
(malloc=123KB #212)
(arena=39KB #68)

- Code (reserved=248370KB, committed=12490KB)
(malloc=682KB #3839)
(mmap: reserved=247688KB, committed=11808KB)

- GC (reserved=62483KB, committed=62483KB)
(malloc=10187KB #7071)
(mmap: reserved=52296KB, committed=52296KB)

- Compiler (reserved=146KB, committed=146KB)
(malloc=13KB #307)
(arena=133KB #5)

- Internal (reserved=460KB, committed=460KB)
(malloc=428KB #1421)
(mmap: reserved=32KB, committed=32KB)

- Other (reserved=16KB, committed=16KB)
(malloc=16KB #3)

- Symbol (reserved=6593KB, committed=6593KB)
(malloc=6042KB #72520)
(arena=552KB #1)

- Native Memory Tracking (reserved=1646KB, committed=1646KB)
(malloc=9KB #113)
(tracking overhead=1637KB)

- Shared class space (reserved=17036KB, committed=17036KB)
(mmap: reserved=17036KB, committed=17036KB)

- Arena Chunk (reserved=185KB, committed=185KB)
(malloc=185KB)

- Logging (reserved=4KB, committed=4KB)
(malloc=4KB #191)

- Arguments (reserved=18KB, committed=18KB)
(malloc=18KB #489)

- Module (reserved=124KB, committed=124KB)
(malloc=124KB #1521)

- Synchronizer (reserved=129KB, committed=129KB)
(malloc=129KB #1089)

- Safepoint (reserved=8KB, committed=8KB)
(mmap: reserved=8KB, committed=8KB)

Здесь мы можем заметить подробности о различных типах памяти, кроме Java Heap Memory . Класс определяет память JVM, используемую для хранения метаданных класса . Точно так же поток определяет память, которую используют потоки нашего приложения. И код предоставляет память, используемую для хранения сгенерированного JIT кода, сам компилятор использует некоторое пространство, и GC также занимает некоторое пространство.

Кроме того, зарезервированный может дать оценку памяти, необходимой для нашего приложения. И зафиксированный показывает минимально выделенную память.

5. Диагностика утечки памяти

Давайте посмотрим, как мы можем определить, есть ли утечка памяти в нашей JVM. Следовательно, для начала нам нужно сначала иметь базовый уровень. А затем нужно какое-то время отслеживать, чтобы понять, есть ли постоянное увеличение памяти в любом из типов памяти, упомянутых выше.

Давайте сначала оценим использование памяти JVM, как показано ниже:

root@c6b47b129071:/# jcmd 19 VM.native_memory baseline
19:
Baseline succeeded

Теперь используйте приложение для нормального или интенсивного использования в течение некоторого времени. В конце просто используйте diff , чтобы определить изменение по сравнению с базовым уровнем , как показано ниже:

root@c6b47b129071:/# jcmd 19 VM.native_memory summary.diff
19:

Native Memory Tracking:

Total: reserved=1162150KB +2540KB, committed=660930KB +3068KB

- Java Heap (reserved=524288KB, committed=524288KB)
(mmap: reserved=524288KB, committed=524288KB)

- Class (reserved=281737KB +2085KB, committed=31801KB +2341KB)
(classes #6821 +395)
( instance classes #6315 +355, array classes #506 +40)
(malloc=1161KB +37KB #16648 +750)
(mmap: reserved=280576KB +2048KB, committed=30640KB +2304KB)
( Metadata: )
( reserved=26624KB +2048KB, committed=26544KB +2048KB)
( used=25790KB +1947KB)
( free=754KB +101KB)
( waste=0KB =0.00%)
( Class space:)
( reserved=253952KB, committed=4096KB +256KB)
( used=3615KB +245KB)
( free=481KB +11KB)
( waste=0KB =0.00%)

- Thread (reserved=18439KB, committed=2779KB +80KB)
(thread #35)
(stack: reserved=18276KB, committed=2616KB +80KB)
(malloc=123KB #212)
(arena=39KB #68)

- Code (reserved=248396KB +21KB, committed=12772KB +213KB)
(malloc=708KB +21KB #3979 +110)
(mmap: reserved=247688KB, committed=12064KB +192KB)

- GC (reserved=62501KB +16KB, committed=62501KB +16KB)
(malloc=10205KB +16KB #7256 +146)
(mmap: reserved=52296KB, committed=52296KB)

- Compiler (reserved=161KB +15KB, committed=161KB +15KB)
(malloc=29KB +15KB #341 +34)
(arena=133KB #5)

- Internal (reserved=495KB +35KB, committed=495KB +35KB)
(malloc=463KB +35KB #1429 +8)
(mmap: reserved=32KB, committed=32KB)

- Other (reserved=52KB +36KB, committed=52KB +36KB)
(malloc=52KB +36KB #9 +6)

- Symbol (reserved=6846KB +252KB, committed=6846KB +252KB)
(malloc=6294KB +252KB #76359 +3839)
(arena=552KB #1)

- Native Memory Tracking (reserved=1727KB +77KB, committed=1727KB +77KB)
(malloc=11KB #150 +2)
(tracking overhead=1716KB +77KB)

- Shared class space (reserved=17036KB, committed=17036KB)
(mmap: reserved=17036KB, committed=17036KB)

- Arena Chunk (reserved=186KB, committed=186KB)
(malloc=186KB)

- Logging (reserved=4KB, committed=4KB)
(malloc=4KB #191)

- Arguments (reserved=18KB, committed=18KB)
(malloc=18KB #489)

- Module (reserved=124KB, committed=124KB)
(malloc=124KB #1528 +7)

- Synchronizer (reserved=132KB +3KB, committed=132KB +3KB)
(malloc=132KB +3KB #1111 +22)

- Safepoint (reserved=8KB, committed=8KB)
(mmap: reserved=8KB, committed=8KB)

Со временем, пока работает GC, мы заметим увеличение и уменьшение использования памяти. Однако если происходит неконтролируемое увеличение использования памяти, это может быть проблемой утечки памяти. Следовательно, по этой статистике мы можем определить область утечки памяти, такую как куча , поток, код, класс и т. д. И если нашему приложению требуется больше памяти, мы можем соответствующим образом настроить соответствующие аргументы виртуальной машины.

Если утечка памяти происходит в куче , мы можем сделать дамп кучи (как объяснялось ранее) или просто настроить Xmx . Точно так же, если утечка памяти происходит в Thread, мы можем искать необработанные рекурсивные инструкции или настраивать Xss .

6. Заключение

В этой статье мы рассмотрели утилиту для диагностики JVM для различных сценариев.

Мы также рассмотрели команду jcmd и ее различное использование для получения дампа кучи, дампа потока, записи JFR для различных анализов, связанных с производительностью. В конце концов, мы также рассмотрели способ диагностики утечки памяти с помощью jcmd .