Advanced Java-02d-JVM分析工具

JDK提供的命令行

以下每个工具都可以 cmd --help的方式查看说明

jps

查看当前用户启动jvm进程

  • jps -m: 显示传递给Main方法的参数
  • jps -l: 显示Java进程的完整包名
  • jps -v: 显示JVM的参数
  • jps -lvm

jvm启动后会在/tmp/hsperfdata_<username>/目录下生成一个pid为名的文件, 这个目录由-Djava.io.tmpdir参数指定, 如果因为某些原因这个文件没有生成, jps也就不起作用
文件内容:?

jstat

查看每个分代的使用率和GC次数,在没有GUI图形的服务器上是运行期定位虚拟机性能问题的首选。

注意jstat返回只有 YGC和 FGC,并不区分 Major GC和 Full GC;
jstat和-XX:+PrintGCDetails提供的结果有不同,在于:
jstat无法统计并行的任务,比如UseConcMarkSweepGC情况下,初始mark和remark阶段都会有 Stop the World的耗时,jstat的输出会把两个STW阶段视作两次 Full GC;
而在GC日志里可以清楚的看到 UseConcMarkSweepGC情况下,每个阶段的耗时。

例如, 如果配置了CMS垃圾回收器,那么 jstat中的 FGC增加1并不表示就一定发生了 Full GC,很有可能是发生了老年代的 CMS GC,而且每发生一次老年代的 CMS GC,jstat中的 FGC就会+2

常用jstat命令:

  • jstat -gc pid: 统计JVM内存(Young/Old/Method)的已使用/总空间大小,以及Young GC和Full GC发生次数和耗时;
  • jstat -gcutil pid : 统计JVM内存(Young/Old/Method)的占用百分比,以及Young GC和Full GC发生次数和耗时;
  • jstat -class pid : 类装载、卸载数量、总空间, 及类装载所耗费的时间
  • jstat -gccapacity pid : 查看三代(young,old,perm)对象的使用量大小(字节)
  • jstat -gcnew pid: 年轻代的容量和GC情况
  • jstat -gcnewcapacity pid:
  • jstat -gcold pid: 老年代的容量和GC情况
  • jstat -gcoldcapacity pid:

如果用jstat查看远程机器上的jvm, 需要在远程主机启动jstatd(详见 jstatd)

jstat -gc pid@remote_IP # 用jstat连接远端的jstatd

jstat -gc返回列解析

示例1: attaches 到pid=14542的进程上, -h3表示每三行打印一次列名称, 采样间隔5s

jstat -gc -h3 14542 5s

S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
10240.0 10752.0 0.0 0.0 133120.0 10215.4 87552.0 9225.3 27904.0 26661.4 3328.0 3007.2 20 0.305 13 1.410 1.715
10240.0 10752.0 0.0 0.0 133120.0 10659.2 87552.0 9225.3 27904.0 26661.4 3328.0 3007.2 20 0.305 13 1.410 1.715
10240.0 10752.0 0.0 0.0 133120.0 10659.2 87552.0 9225.3 27904.0 26661.4 3328.0 3007.2 20 0.305 13 1.410 1.715
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
10240.0 512.0 0.0 0.0 133120.0 79.2 87552.0 9224.0 27904.0 26662.1 3328.0 3007.2 21 0.318 14 1.522 1.840
10240.0 512.0 0.0 0.0 133120.0 523.0 87552.0 9224.0 27904.0 26662.1 3328.0 3007.2 21 0.318 14 1.522 1.840
10240.0 512.0 0.0 0.0 133120.0 523.0 87552.0 9224.0 27904.0 26662.1 3328.0 3007.2 21 0.318 14 1.522 1.840

每列说明如下:

  • S0C: S0 Capacity(KB)
  • S0U: S0 Utilization(KB)
  • EC: Current eden space capacity (kB).
  • EU: Eden space utilization (kB).
  • MC: Metaspace capacity (kB).
  • MU: Metacspace utilization (kB).
  • YGC: 从JVM进程启动到当前采样,发生young gen GC总次数
  • YGCT: 从JVM进程启动到当前采样,young gen GC总消耗时间(秒), 相邻两次相减就是该次耗时
  • FGC: 从JVM进程启动到当前采样,发生full GC总次数
  • FGCT: 从JVM进程启动到当前采样,full GC总消耗时间(秒), 相邻两次相减就是该次耗时

注: 上面是java 1.8的jstat的返回值, 可以看到有Metaspace(元空间), 如果是java 1.7或更老版本, 则没有MC/MU, 而是PC/PU:

  • PC:Perm Capacity(KB)
  • PU: Perm Utilization(KB)

jstat -gcutil返回列解析

示例2: attaches 到pid=14542的进程上, -h3表示每三行打印一次列名称, 采样间隔250ms

jstat -gcutil -h3 14542 3s

S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
16.35 0.00 94.62 63.31 97.41 94.84 4522 83.661 32 0.931 84.592
16.35 0.00 95.52 63.31 97.41 94.84 4522 83.661 32 0.931 84.592
16.35 0.00 96.92 63.31 97.41 94.84 4522 83.661 32 0.931 84.592
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 14.90 1.43 63.31 97.41 94.84 4523 83.678 32 0.931 84.609
0.00 14.90 2.65 63.31 97.41 94.84 4523 83.678 32 0.931 84.609
0.00 14.90 3.68 63.31 97.41 94.84 4523 83.678 32 0.931 84.609

上面发生了一次Young GC, S0从16.35%降到0%, S1从0%增长到14.90%, Eden从96.92%降到1.43, 耗时0.017s

以上参考Oracle Java 8 jstat手册:
jstat

jstatd

jstatd是一个 RMI的server,它可以监控 Hotspot的JVM的启动和结束,同时提供接口可以让远程机器连接到JVM。 比如 jstat / JVisualVM 都可以通过jstatd来远程观察JVM的运行情况。
在远程服务器上启动jstatd: nohup jstatd -J-Djava.security.policy=/home/xxx/jstatd.all.policy -J-Djava.rmi.server.hostname=192.168.0.2 -p 1099 & , 1099是jstatd的默认端口

jstatd.all.policy内容如下:

grant codebase "file:${java.home}/../lib/tools.jar" {
permission java.security.AllPermission;
};

jstack

查看jvm进程的线程状态, 也可以做线程的dump,
jstack pid : 查看当前所有线程的运行栈, 包括线程当前状态(blocked, waitting), 线程占用了哪个对象锁, 线程在等待哪个对象锁;

$ jstack 18303 |more
Picked up JAVA_TOOL_OPTIONS: -Duser.language=en
2023-05-17 23:20:41
Full thread dump OpenJDK 64-Bit Server VM (11.0.15+10-b2043.56 mixed mode):

"Finalizer" #3 daemon prio=8 os_prio=31 cpu=385.10ms elapsed=657893.45s tid=0x00007feb5f0b2000 nid=0x4103 in Object.wait() [0x000070000d3db000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(java.base@11.0.15/Native Method)
- waiting on <no object reference available>
at java.lang.ref.ReferenceQueue.remove(java.base@11.0.15/ReferenceQueue.java:155)
- waiting to re-lock in wait() <0x00000007c047ac10> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(java.base@11.0.15/ReferenceQueue.java:176)
at java.lang.ref.Finalizer$FinalizerThread.run(java.base@11.0.15/Finalizer.java:170)

"Signal Dispatcher" #4 daemon prio=9 os_prio=31 cpu=3.19ms elapsed=657893.40s tid=0x00007feb5e80d800 nid=0x5703 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"Service Thread" #5 daemon prio=9 os_prio=31 cpu=141.17ms elapsed=657893.39s tid=0x00007feb5f0c5800 nid=0x5803 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #6 daemon prio=9 os_prio=31 cpu=1165586.22ms elapsed=657893.39s tid=0x00007feb5f0cd000 nid=0x5b03 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
No compile task

"C1 CompilerThread0" #7 daemon prio=9 os_prio=31 cpu=127074.31ms elapsed=657893.39s tid=0x00007feb6003b800 nid=0x5c03 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
No compile task

jmap

查看堆内存的情况, 也可以生成堆内存的dump信息,
jmap dump会触发Full GC, 所以在生产环境要小心使用.

  • jmap -heap pid: 打印Heap(新生代/老年代/永久代等等..)的size参数和实际占用
  • jmap -histo pid: 打印出每个类的对象数量, 以及占用内存。如果出现jvm堆占用率过高,可以用histo查看哪个类的对象最多,猜测出哪里的代码有问题
  • jmap -histo:live pid: 只打印存活的
  • jmap -dump:format=b,file=FileName 6900: 把内存详细使用情况dump到文件(小心, 这个命令可能会暂停当前应用)
    • -dump:[live,]format=b,file=FileName: live指只有活动的对象被转储到dump文件

如果程序内存不足或者频繁GC,很有可能存在内存泄露情况:

  1. 可以先使用jmap -heap命令查看堆的使用情况,看一下各个堆空间的占用情况。
  2. 使用jmap -histo:[live]查看堆内存中的对象的情况。如果有大量对象在持续被引用,并没有被释放掉,那就产生了内存泄露,就要结合代码,为什么没有被释放
  3. 也可以使用 jmap -dump:format=b,file=<fileName>命令将堆信息保存到一个文件中,再借助jhat命令查看详细内容
  4. 在内存出现泄露、溢出或者其它前提条件下,建议多dump几次内存,把内存文件进行编号归档,便于后续内存整理分析。

图-使用jmap -histo pid 按类型统计存活类的个数:
jmap-histo-live

jhat

查看jmap转储的二进制文件

  • jhat -port 5000 FileName : 在本地启动http服务显示jmap生成的dump文件信息, 在http://localhost:5000 查看

总结:对Jvm进程进行堆栈Dump的方法

jstack可以生成Jvm线程的堆栈dump文件, jmap可以生成堆栈的dump文件,
让虚拟机在内存不足时自动生成dump文件: -XX:+HeapDumpOnOutOfMemoryError
图形化的dump生成工具: Java VisualVM

jcmd

1.7之后新增, 有多种功能的命令集合, 命令格式: jcmd $PID $Command, 查看可用的Command: jcmd $PID help, “Oracle官方建议使用jcmd代替jmap”

  • jcmd -l: 类似jps -m
  • jcmd pid Thread.print : 打印当前堆栈
  • jcmd pid GC.heap_dump /tmp/dumpFile : 导出dump文件
  • jcmd pid VM.system_properties : 打印出该进程所有-D参数

jinfo

  • jinfo pid, 获取jvm进程的所有参数, 后续版本可能会移除这个工具

jdb

  • 被调试的java进程启动参数-Xdebug -Xrunjdwp:transport=dt_socket,address=8787
  • 连接到上面的进程进行debug: jdb -attach 192.168.1.79:8787 -sourcepath .

JConsole

Java 5提供的JConsole

JProfiler

JProfiler是由ej-technologies GmbH开发的商业授权的性能分析工具.

参考: 深入浅出JProfiler-博客-云栖社区-阿里云 @ref

VisualVM

VisualVM 是一个性能分析工具,自从 JDK 6 Update 7 以后已经作为 Oracle JDK 的一部分,位于 JDK 根目录的 bin 文件夹下。
以下参考自: 使用 VisualVM 进行性能分析及调优 @ref

使用VisualVM需要远程服务器上运行一个jstatd守护进程, 或者远程服务器上运行的Java Application启用了JMX, 应用程序添加如下参数来启动JMX:

-Dcom.sun.management.jmxremote=true
-Dcom.sun.management.jmxremote.port=9090
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false

JVisualVM UI

JVisualVM连接到JVM线程使用了Attach API, 在本文档搜索Attach API.
启动命令: jvisualvm

Btrace

BTrace是SUN Kenai云计算开发平台下的一个开源项目,旨在为java提供安全可靠的动态跟踪分析工具。

Btrace能用来做什么?
举例, 如果要对线上运行的Java程序进行调试, 可以通过在代码里加入debug打印信息来实现, 但缺点也很明显, 需要不断地修改代码,加入System.out.println(), 还需要不断重启应用程序. 对于线上服务这是不可接受的.

Btrace可以改变上面低效的调试方式, Btrace可以使用类似AOP式的代码植入, 在我们关心的代码位置插入自定义代码, 比如:
在每个方法结束都打印耗时, 统计最耗时的方法;
ArrayList.add里加入代码, 如果size过大则打印log, 找出超大的ArrayList;
System.gc()被调用时, 打印出调用堆栈, 找出是哪里在调用gc;

并且最重要的是, 使用Btrace不需要重新编译项目代码, 也不需要重启进程, 所以Btrace非常适合在线上发生异常的环境上进行调试埋点.

Btrace的使用

Btrece的使用:

  1. 启动Java程序
  2. 编写Btrace代码, 用注解指定要切入的类和方法
  3. 用btracec编译上面的代码
  4. 用btrace命令把 agent.jar attach到运行中的Java进程, agent.jar启动端口, Btrece Client 使用这个端口发送命令和.class字节码

更多BTrace使用例子:

Btrace用到的技术介绍

Btrace 使用 Java Complier API 编译切入代码, 生成.class文件;

再使用 Attach API 把 agent.jar 附加到目标JVM上,agent 启动端口,接受来自 Client的指令和class字节码,
当agent接收到监控命令后,主要有以下两部分工作:

  • 重写类:遍历当前所有的class,根据正则找到匹配的类,使用asm重写被切入类的字节码(CGLIB代理也用到了asm)
  • 替换类:使用替换掉原来的class,使用了 Instrumentation API ,instrumentation的retransformClasses方法将原始字节码替换掉

Attach APIInstrumentation API 都是JVM Tool Interface (JVMTI)里提供的工具类;
有关JVMTI相关的链接:

http://docs.oracle.com/javase/7/docs/platform/jvmti/jvmti.html
https://github.com/jon-bell/bytecode-examples

参考:

HouseMD

比BTrace更轻量级的Java进程运行时的诊断调式命令行工具,可以用来跟踪跟踪方法的耗时。

Memory Analyzer (MAT)

Java Heap Dump 文件(通过jmap -dump转储的文件)分析工具,可以分析堆内存中每种对象的数量,还可以跟踪对象的引用链,排查内存泄漏问题。