Java JVM: monitorización de rendimiento desde CLI

En esta entrada voy a dar un repaso a las herramientas que podemos utilizar desde línea de comandos (CLI) para monitorizar el rendimiento de una Java Virtual Machine (JVM). Como complemento a esta entrada podéis leer la que publiqué hace unos días sobre como generar un Java thread dump con OpenJDK y Tomcat ya que los comandos/herramientas jstack y jps es parte importante de este tipo de monitorización, lo tenéis explicado en ese artículo con mayor detalle. También podéis revisar como acceder a una monitorización de Java a través de JXM y jConsole en modo gráfico.

Los comandos básicos que podemos utilizar para monitorizar una JVM desde CLI son los siguientes:

  • jps
  • jstat
  • jstack
  • jcmd

jps

Como ya comenté en la entrada que he citado anteriormente sobre generar un Java Thread Dump, el comando jps simplemente muestra por salida estándar los PID (lvmid) de los procesos java que hay ejecutandose en el sistema junto con el nombre de la clase que se está utilizando y sus argumentos. Es como el comando ps de *nix pero adaptado a Java.

Modo de utilización:

jps [-q] [-mlvV] [<hostid>]

Ejemplo:

# jps -mlvV
1645 org.apache.catalina.startup.Bootstrap start 
-Djavax.sql.DataSource.Factory=org.apache.commons.dbcp.BasicDataSourceFactory 
-Dcatalina.base=/usr/share/tomcat6 -Dcatalina.home=/usr/share/tomcat6 
-Djava.endorsed.dirs= -Djava.io.tmpdir=/var/cache/tomcat6/temp 
-Djava.util.logging.config.file=/usr/share/tomcat6/conf/logging.properties 
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
1691 sun.tools.jps.Jps -mlvV -Dapplication.home=/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.71 -Xms8m

Este comando nos servirá para sacar el PID de la JVM, necesario para poder empezar la monitorización del proceso con el resto de comandos (jstat, jstack…).

jcmd

El comando jcmd (ojo, disponible a partir de JDK 7) permite consultar información sobre las JVM al igual que jps, es más, si lo ejecutamos sin parámetros la información es la misma:

# jcmd 
1645 org.apache.catalina.startup.Bootstrap start
1709 sun.tools.jcmd.JCmd
# jcmd -l
1718 sun.tools.jcmd.JCmd -l
1645 org.apache.catalina.startup.Bootstrap start

Para que nos muestre información más específica y no disponible con jps utilizamos los parámeteros que nos ofrece. Si atacamos al PID de una JVM concreta sí que podemos ver información de utilidad sobre ella que no nos ofrecía jps:

$ jcmd 1645 VM.system_properties
1645:
#Sat Nov 01 10:20:55 CET 2014
java.runtime.name=OpenJDK Runtime Environment
sun.boot.library.path=/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.71/jre/lib/i386
java.vm.version=24.65-b04
shared.loader=
java.vm.vendor=Oracle Corporation
java.vendor.url=http\://java.oracle.com/
path.separator=\:
java.vm.name=OpenJDK Client VM
tomcat.util.buf.StringCache.byte.enabled=true
file.encoding.pkg=sun.io
java.util.logging.config.file=/usr/share/tomcat6/conf/logging.properties
user.country=US
sun.java.launcher=SUN_STANDARD
sun.os.patch.level=unknown
java.vm.specification.name=Java Virtual Machine Specification
user.dir=/usr/share/tomcat6
java.runtime.version=1.7.0_71-mockbuild_2014_10_17_21_54-b00
java.awt.graphicsenv=sun.awt.X11GraphicsEnvironment
java.endorsed.dirs=
os.arch=i386
javax.accessibility.assistive_technologies=org.GNOME.Accessibility.JavaBridge
java.io.tmpdir=/var/cache/tomcat6/temp
line.separator=\n
java.vm.specification.vendor=Oracle Corporation
java.util.logging.manager=org.apache.juli.ClassLoaderLogManager
java.naming.factory.url.pkgs=org.apache.naming
os.name=Linux
sun.jnu.encoding=UTF-8
java.library.path=/usr/java/packages/lib/i386\:/lib\:/usr/lib
java.specification.name=Java Platform API Specification
java.class.version=51.0
sun.management.compiler=HotSpot Client Compiler
os.version=2.6.32-279.el6.i686
user.home=/usr/share/tomcat6
catalina.useNaming=true
user.timezone=Europe/Madrid
java.awt.printerjob=sun.print.PSPrinterJob
file.encoding=UTF-8
java.specification.version=1.7
catalina.home=/usr/share/tomcat6
java.class.path=\:/usr/share/tomcat6/bin/bootstrap.jar\:/usr/share/tomcat6/bin/tomcat-juli.jar\:/usr/share/java/commons-daemon.jar
user.name=tomcat
java.naming.factory.initial=org.apache.naming.java.javaURLContextFactory
package.definition=sun.,java.,org.apache.catalina.,org.apache.coyote.,org.apache.tomcat.,org.apache.jasper.
javax.sql.DataSource.Factory=org.apache.commons.dbcp.BasicDataSourceFactory
java.vm.specification.version=1.7
sun.java.command=org.apache.catalina.startup.Bootstrap start
java.home=/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.71/jre
sun.arch.data.model=32
user.language=en
java.specification.vendor=Oracle Corporation
awt.toolkit=sun.awt.X11.XToolkit
java.vm.info=mixed mode, sharing
java.version=1.7.0_71
java.ext.dirs=/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.71/jre/lib/ext\:/usr/java/packages/lib/ext
sun.boot.class.path=/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.71/jre/lib/resources.jar\:/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.71/jre/lib/rt.jar\:/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.71/jre/lib/sunrsasign.jar\:/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.71/jre/lib/jsse.jar\:/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.71/jre/lib/jce.jar\:/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.71/jre/lib/charsets.jar\:/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.71/jre/lib/rhino.jar\:/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.71/jre/lib/jfr.jar\:/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.71/jre/classes
server.loader=
java.vendor=Oracle Corporation
catalina.base=/usr/share/tomcat6
file.separator=/
java.vendor.url.bug=http\://bugreport.sun.com/bugreport/
common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar
sun.font.fontmanager=sun.awt.X11FontManager
sun.io.unicode.encoding=UnicodeLittle
sun.cpu.endian=little
package.access=sun.,org.apache.catalina.,org.apache.coyote.,org.apache.tomcat.,org.apache.jasper.,sun.beans.
sun.cpu.isalist=

Ya tenemos un volcado con un montón de información de la JVM. Vemos información relacionada con:

  • VM.native_memory
  • ManagementAgent.stop
  • ManagementAgent.start_local
  • ManagementAgent.start
  • Thread.print
  • GC.class_histogram
  • GC.heap_dump
  • GC.run_finalization
  • GC.run
  • VM.uptime
  • VM.flags
  • VM.system_properties
  • VM.command_line
  • VM.version

Podemos invocar por separado cada propiedad para evitar un volcado tan grande de información. Si queremos ver, por ejemplo, información de los Threads en ejecución ejecutaríamos lo siguiente. Si os fijáis, es lo mismo que la ejecución del jstack, que utilizabamos para generar un thread dump:

$ jcmd 1645 Thread.print
1645:
2014-11-01 10:25:00
Full thread dump OpenJDK Client VM (24.65-b04 mixed mode, sharing):

"Attach Listener" daemon prio=10 tid=0xb763b800 nid=0x6f6 runnable [0x00000000]
   java.lang.Thread.State: RUNNABLE

"TP-Monitor" daemon prio=10 tid=0xb763a000 nid=0x69a in Object.wait() [0xb47ba000]
   java.lang.Thread.State: TIMED_WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on  (a org.apache.tomcat.util.threads.ThreadPool$MonitorRunnable)
	at org.apache.tomcat.util.threads.ThreadPool$MonitorRunnable.run(ThreadPool.java:565)
	- locked  (a org.apache.tomcat.util.threads.ThreadPool$MonitorRunnable)
	at java.lang.Thread.run(Thread.java:745)

"TP-Processor4" daemon prio=10 tid=0xb7638400 nid=0x699 runnable [0xb480b000]
   java.lang.Thread.State: RUNNABLE
	at java.net.PlainSocketImpl.socketAccept(Native Method)
	at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:398)
	at java.net.ServerSocket.implAccept(ServerSocket.java:530)
	at java.net.ServerSocket.accept(ServerSocket.java:498)
	at org.apache.jk.common.ChannelSocket.accept(ChannelSocket.java:307)
	at org.apache.jk.common.ChannelSocket.acceptConnections(ChannelSocket.java:661)
	at org.apache.jk.common.ChannelSocket$SocketAcceptor.runIt(ChannelSocket.java:872)
	at org.apache.tomcat.util.threads.ThreadPool$ControlRunnable.run(ThreadPool.java:690)
	at java.lang.Thread.run(Thread.java:745)

"TP-Processor3" daemon prio=10 tid=0xb7636800 nid=0x698 in Object.wait() [0xb485c000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on  (a org.apache.tomcat.util.threads.ThreadPool$ControlRunnable)
	at java.lang.Object.wait(Object.java:503)
	at org.apache.tomcat.util.threads.ThreadPool$ControlRunnable.run(ThreadPool.java:662)
	- locked  (a org.apache.tomcat.util.threads.ThreadPool$Co
[...]

Las posibilidades son muy amplias, también podemos, por ejemplo, visualizar un volcado de todos los contadores relacionados con el rendimiento de la JVM:

$ jcmd 1645 GC.heap_dump PerfCounter.print 
1645:
java.ci.totalTime=1455540
java.cls.loadedClasses=941
java.cls.sharedLoadedClasses=651
java.cls.sharedUnloadedClasses=0
java.cls.unloadedClasses=0
java.property.java.class.path=":/usr/share/tomcat6/bin/bootstrap.jar:/usr/share/tomcat6/bin/tomcat-juli.jar:/usr/share/java/commons-daemon.jar"
java.property.java.endorsed.dirs=""
java.property.java.ext.dirs="/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.71/jre/lib/ext:/usr/java/packages/lib/ext"
java.property.java.home="/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.71/jre"
java.property.java.library.path="/usr/java/packages/lib/i386:/lib:/usr/lib"
java.property.java.version="1.7.0_71"
java.property.java.vm.info="mixed mode, sharing"
java.property.java.vm.name="OpenJDK Client VM"
java.property.java.vm.specification.name="Java Virtual Machine Specification"
java.property.java.vm.specification.vendor="Oracle Corporation"
java.property.java.vm.specification.version="1.7"
java.property.java.vm.vendor="Oracle Corporation"
java.property.java.vm.version="24.65-b04"
java.rt.vmArgs="-Djavax.sql.DataSource.Factory=org.apache.commons.dbcp.BasicDataSourceFactory -Dcatalina.base=/usr/share/tomcat6 -Dcatalina.home=/usr/share/tomcat6 -Djava.endorsed.dirs= -Djava.io.tmpdir=/var/cache/tomcat6/temp -Djava.util.logging.config.file=/usr/share/tomcat6/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager"
java.rt.vmFlags=""
java.threads.daemon=12
java.threads.live=13
java.threads.livePeak=13
java.threads.started=13
sun.ci.compilerThread.0.compiles=309
sun.ci.compilerThread.0.method=""
sun.ci.compilerThread.0.time=6089
sun.ci.compilerThread.0.type=1
sun.ci.lastFailedMethod=""
sun.ci.lastFailedType=0
sun.ci.lastInvalidatedMethod=""
sun.ci.lastInvalidatedType=0
sun.ci.lastMethod="java/lang/String getBytes"
sun.ci.lastSize=27
sun.ci.lastType=1
sun.ci.nmethodCodeSize=228688
sun.ci.nmethodSize=575040
sun.ci.osrBytes=0
sun.ci.osrCompiles=0
sun.ci.osrTime=0
sun.ci.standardBytes=52071
sun.ci.standardCompiles=309
sun.ci.standardTime=1455540
sun.ci.threads=1
sun.ci.totalBailouts=0
sun.ci.totalCompiles=309
sun.ci.totalInvalidates=0
sun.classloader.findClassTime=1961005332
sun.classloader.findClasses=251
sun.classloader.parentDelegationTime=899720500
sun.cls.appClassBytes=1329461
sun.cls.appClassLoadCount=488
sun.cls.appClassLoadTime=2268616
sun.cls.appClassLoadTime.self=1166487
sun.cls.classInitTime=5150627
sun.cls.classInitTime.self=3333236
sun.cls.classLinkedTime=1528211
sun.cls.classLinkedTime.self=144025
sun.cls.classVerifyTime=1313279
sun.cls.classVerifyTime.self=483593
sun.cls.defineAppClassTime=655180
sun.cls.defineAppClassTime.self=69185
sun.cls.defineAppClasses=271
sun.cls.initializedClasses=1181
sun.cls.isUnsyncloadClassSet=0
sun.cls.jniDefineClassNoLockCalls=0
sun.cls.jvmDefineClassNoLockCalls=38
sun.cls.jvmFindLoadedClassNoLockCalls=1075
sun.cls.linkedClasses=1420
sun.cls.loadInstanceClassFailRate=0
sun.cls.loadedBytes=1131600
sun.cls.lookupSysClassTime=2295064
sun.cls.methodBytes=800392
sun.cls.nonSystemLoaderLockContentionRate=1
sun.cls.parseClassTime=1700604
sun.cls.parseClassTime.self=1159188
sun.cls.sharedClassLoadTime=1412946
sun.cls.sharedLoadedBytes=816344
sun.cls.sharedUnloadedBytes=0
sun.cls.sysClassBytes=2491030
sun.cls.sysClassLoadTime=3954269
sun.cls.systemLoaderLockContentionRate=0
sun.cls.time=8982118
sun.cls.unloadedBytes=0
sun.cls.unsafeDefineClassCalls=20
sun.cls.verifiedClasses=824
sun.gc.cause="No GC"
sun.gc.collector.0.invocations=5
sun.gc.collector.0.lastEntryTime=11849108
sun.gc.collector.0.lastExitTime=11871701
sun.gc.collector.0.name="Copy"
sun.gc.collector.0.time=192835
sun.gc.collector.1.invocations=0
sun.gc.collector.1.lastEntryTime=0
sun.gc.collector.1.lastExitTime=0
sun.gc.collector.1.name="MSC"
sun.gc.collector.1.time=0
sun.gc.generation.0.agetable.bytes.00=0
sun.gc.generation.0.agetable.bytes.01=393216
sun.gc.generation.0.agetable.bytes.02=0
sun.gc.generation.0.agetable.bytes.03=0
sun.gc.generation.0.agetable.bytes.04=0
sun.gc.generation.0.agetable.bytes.05=0
[...]

La información puede ser abrumadora, lo sé, así que lo recomendable es localizar los parámetros y valores que realmente necesitamos para nuestra monitorización del rendimiento de la JVM y procesarlos como estimemos oportuno (scripts, gráficas, etc).

jstat

El comando jstat muestra información detallada y estadísticas de la JVM.

Lo más básico a conocer es la forma de mostrar información sobre el Garbage Collector (GC). Para ello se utiliza el parámetro -gctuil junto con el PID de la JVM que obtenemos con jps o jcmd:

$ jstat -gcutil 1645
  S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT   
  0.00 100.00  44.35  38.48  31.63      5    0.193     0    0.000    0.193

Para visualizar la información a tiempo real en un intervalo específico de tiempo añadimos como parámetro el tiempo en milisegundos. En el siguiente ejemplo se muestra la información cada 2 segundos para la JVM con el PID 1645:

$ jstat -gcutil 1645 2000
  S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT   
  0.00 100.00  44.35  38.48  31.63      5    0.193     0    0.000    0.193
  0.00 100.00  44.35  38.48  31.63      5    0.193     0    0.000    0.193
  0.00 100.00  44.35  38.48  31.63      5    0.193     0    0.000    0.193
[...]

La explicación de los valores de cada columna son los siguientes:

  • S0: Survivor space 0. Porcentaje de utilización sobre la capacidad total del espacio de memoria.
  • S1: Survivor space 1. Porcentaje de utilización sobre la capacidad total del espacio de memoria.
  • E: Eden space. Porcentaje de utilización sobre la capacidad total del espacio de memoria.
  • O: Old space. Porcentaje de utilización sobre la capacidad total del espacio de memoria.
  • P: Permanent space. Porcentaje de utilización sobre la capacidad total del espacio de memoria.
  • YGC: número de eventos de «young generation GC» generados.
  • YGCT: Tiempo de garbage collection utilizada por los eventos «Young generation».
  • FGC: número total de eventos en el GC.
  • FGCT: tiempo utilizado para un GC completo.
  • GCT: tiempo total utilizado para eventos de GC.

jstack

Al comienzo del artículo tenéis el link a la entrada en la que hablo sobre jstack. Básicamente jstack permite generar un «thread dump» de Java, es decir, un volcado de todos los hilos que hay en ejecución en una JVM (Java Virtual Machine).

$ jstack 1645 | more
2014-11-01 10:44:01
Full thread dump OpenJDK Client VM (24.65-b04 mixed mode, sharing):

"Attach Listener" daemon prio=10 tid=0xb763b800 nid=0x6f6 runnable [0x00000000]
   java.lang.Thread.State: RUNNABLE

"TP-Monitor" daemon prio=10 tid=0xb763a000 nid=0x69a in Object.wait() [0xb47ba000]
   java.lang.Thread.State: TIMED_WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on  (a org.apache.tomcat.util.threads.ThreadPool$MonitorRunnable)
	at org.apache.tomcat.util.threads.ThreadPool$MonitorRunnable.run(ThreadPool.java:565)
	- locked  (a org.apache.tomcat.util.threads.ThreadPool$MonitorRunnable)
	at java.lang.Thread.run(Thread.java:745)

"TP-Processor4" daemon prio=10 tid=0xb7638400 nid=0x699 runnable [0xb480b000]
   java.lang.Thread.State: RUNNABLE
	at java.net.PlainSocketImpl.socketAccept(Native Method)
	at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:398)
	at java.net.ServerSocket.implAccept(ServerSocket.java:530)
	at java.net.ServerSocket.accept(ServerSocket.java:498)
	at org.apache.jk.common.ChannelSocket.accept(ChannelSocket.java:307)
	at org.apache.jk.common.ChannelSocket.acceptConnections(ChannelSocket.java:661)
	at org.apache.jk.common.ChannelSocket$SocketAcceptor.runIt(ChannelSocket.java:872)
	at org.apache.tomcat.util.threads.ThreadPool$ControlRunnable.run(ThreadPool.java:690)
	at java.lang.Thread.run(Thread.java:745)
[...]

El dump se genera en STDOUT así que es conveniente redirigirlo a un archivo:

$ jstack 1645 > dump.out

Este comando es muy importante para visualizar si hay threads bloqueados o esperando ya que nos muestra el estado del thread: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING junto con la excepción e información del hilo.

En resumen, conociendo estos comandos, su utilización básica y combinandolos con herramientas gráficas como jConsole y JMX podemos tener una buena visión del estado, rendimiento y propiedades de la JVM y el funcionamiento del servidor de aplicaciones.

2 comentarios en “Java JVM: monitorización de rendimiento desde CLI

  1. En referencia al jstack, ¿sólo tiene sentido cuando usas Runnable o también tiene cabida, por ejemplo, los AsyncTask de Android?

Comments are closed.