JVM之工具篇
简介
这里总结了分析jvm问题时需要用到的工具
工具相关的知识点
为了避免知识点记录的太过分散,把它们集中记录在这里,这些都是使用jvm工具时需要知道的
堆转储文件
Heap Dump,是JVM运行时堆内存的快照,它记录了当前JVM中所有对象的状态和引用关系。堆转储文件通常用于分析内存泄漏、内存溢出等问题。堆转储文件只可以查看内存中有哪些对象,它无法展示这些对象是在代码中的什么位置生成的,这需要用户自己分析。
orcale系列的jvm生成的堆转储文件是hprof格式的。
分析堆转储文件,至少需要文件的1.5倍到2倍内存。
QQL
Object Query Language,对象查询语言,用于分析堆转储文件。可以帮助用户快速定位特定对象、查找大对象等
语法:
SELECT <expression>
FROM <class> <alias>
WHERE <condition>
案例:
- 查找String类的所有实例:SELECT s FROM java.lang.String s
压缩类空间
这个空间是元数据空间的一部分,本质上也是存储类的元数据,它和指针压缩有关。在64位的JVM上,如果堆内存小于32G,会默认开启指针压缩,把8字节的指针压缩为4字节。压缩类空间存储压缩后的类指针
jstat命令统计gc信息时,会统计到这个空间的内存。
java自带的工具
jps
列出目标系统上正在运行的jvm,包括进程ID和main方法所在类的名称
语法:jps [选项] [进程ID]
- 选项:
- -m:显示传递给main方法的实参
- -l:显示应用main方法所在类的包名或jar文件的路径名
- -v:显示传递给jvm的实参
案例:
执行命令:jps -l -m
返回结果:
35845 sun.tools.jps.Jps -l -m
35643 com.intellij.idea.Main
查看虚拟机运行情况的工具
jinfo 查看虚拟机参数
用于查看和动态修改虚拟机运行时配置的参数。
案例:jinfo -flags <pid>
,查看虚拟机的运行时参数
jstat 查看虚拟机的统计信息
查看虚拟机的统计信息,这个命令是实验性的,并且必须要配合选项一起使用
语法:jstat 选项 pid [interval] [count],选项是必选的,通过 jstat -options
查看有哪些可选的option
使用案例
案例1 查看选项
执行命令:jstat -options
结果:
-class
-compiler
-gc
-gccapacity
-gccause
-gcmetacapacity
-gcnew
-gcnewcapacity
-gcold
-gcoldcapacity
-gcutil
-printcompilation
可以看到,可以统计类信息、gc信息、编译信息等
案例2 查看类信息
执行命令:jstat -class ${pid}
结果:
Loaded Bytes Unloaded Bytes Time
534 1083.1 0 0.0 0.04
结果解析:-class选项可以统计JVM的类加载和卸载情况。
结果中的字段:
- Loaded:被加载的类的数量
- Bytes:被加载的类的字节数量
- Unloaded:被卸载的类的数量
- Bytes:被卸载的类的字节数量
- Time:执行类的加载和卸载锁花费的时间
案例3 查看gc信息
执行命令:jstat -gc ${pid}
结果:
结果解析:统计gc相关的信息
结果中的字段:
- S0C、S1C:幸存者空间的容量,单位是kb,就是分代垃圾回收中的from servivor、to servivor
- S0U、S1U:幸存者空间的使用量
- EC、EU:Eden区的容量、使用量
- OC、OC:老年代的容量、使用量
- MC、MU:元空间的容量、使用量
- CCSC 和 CCSU:压缩类空间的容量、使用量
- YGC 和 YGCT:年轻代垃圾回收次数为 0,总耗时为 0 秒。
- FGC 和 FGCT:老年代垃圾回收次数为 0,总耗时为 0 秒。
- GCT:垃圾回收的总耗时为 0 秒。
案例4 查看gc信息 每隔3秒在屏幕上打印1次,1分钟后退出
执行命令:jstat -gc ${pid} 3s 20,每个3s打印一次gc信息,打印20次后退出
jstack 查看虚拟机的线程信息
查看虚拟机的线程信息
jstack 的主要功能:
- 生成线程快照:打印出指定 Java 进程中所有线程的堆栈信息,包括线程的名称、状态、调用栈和锁信息。
- 诊断死锁:通过分析线程的堆栈信息,可以发现线程间的死锁情况,因为死锁总是发生在两个或多个线程之间
语法:jstack [-l | -m | -F] pid
- 选项
- -l:长列表,打印关于锁的信息
- -m:打印java 和 c++的线程信息
使用案例
案例1 查看线程的执行信息
执行命令:jstack ${pid}
输出结果:
打印内容讲解:
"Attach Listener" #11 daemon prio=9 os_prio=31 tid=0x0000000123808800 nid=0x4207 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
以上面这段内容为例:
- “Attach Listener”:线程名称
- #11:表示这是 JVM 中的第 11 个线程
- daemon:守护线程
- prio=9 os_prio=31:优先级和操作系统优先级
- tid:jvm级别的线程id
- nid:操作系统级别的线程id
- java.lang.Thread.State: RUNNABLE:RUNNABLE表示线程正在运行或准备好运行
jmap 查看堆内存
用于生成 Java 堆转储文件和查看JVM的内存使用情况。它通常用于诊断内存泄漏、内存溢出等问题
语法:jmap [options] pid,默认打印共享对象内存信息
- 选项:
- -heap:打印堆内存信息
使用案例
案例1 查看堆内存统计信息
执行命令:jmap -heap ${pid}
结果:
1、堆内存统计信息:
堆配置参数
- MinHeapFreeRatio = 40:堆内存的最小空闲比例。当堆内存的空闲比例低于此值时,JVM 会尝试扩展堆内存。
- MaxHeapFreeRatio = 70:堆内存的最大空闲比例。当堆内存的空闲比例高于此值时,JVM 会尝试收缩堆内存。
- MaxHeapSize = 383778816 (366.0MB):堆的最大大小(-Xmx 参数)。
- NewSize = 8388608 (8.0MB):新生代的初始大小(-Xmn 参数的一部分)。
- MaxNewSize = 127926272 (122.0MB):新生代的最大大小。
- OldSize = 16777216 (16.0MB):老年代的初始大小。
- NewRatio = 2:新生代与老年代的比例(新生代:老年代 = 1:2)。
- SurvivorRatio = 8:Eden 区与 Survivor 区的比例(Eden : Survivor = 8:1)。
- MetaspaceSize = 21807104 (20.796875MB):元空间的初始大小。
- MaxMetaspaceSize = 1759218604032 (1610.0GB):元空间的最大大小(默认值通常很大)。
- G1HeapRegionSize = 0 (0.0MB):G1 垃圾回收器的区域大小。
2、堆内存使用情况
tenured generation是老年代,interned String是字符串常量池中的字符串数量
案例2 查看对象的统计信息
执行命令:jmap -histo <pid>
输出内容讲解:
- class name字段中的值:[C 表示字符数组、[B 表示字节数组、[I 表示int数组,数组前面会有前缀 [,这是字节码指令中的知识点
这个命令可以帮助用户快速定位实例数量过多或占用内存过大的类
案例3 生成堆转储文件
执行命令:jmap -dump:format=b,file=文件名.hprof ${pid}
,文件会生成到当前目录下,这是一个二进制文件,存储了堆的快照信息,需要使用专业工具查看
jcmd 向jvm发送命令
一个命令行工具,用于向正在运行的Java进程发送诊断命令。它提供了多种功能,包括生成堆转储、查看线程信息、触发垃圾回收等
使用案例
案例1 查看帮助命令
执行命令:jcmd <pid> help
,它可以获取指定jvm实例支持的命令
案例2 生成堆转储文件
执行命令:jcmd <pid> GC.heap_dump 文件名
,会生成到程序目录下
堆转储文件是jvm的快照文件,存储了jvm在某一刻的执行信息。
案例3 查看类加载器的统计信息
执行命令: jcmd <pid> VM.classloader_stats
结果:
统计了每个类加载器加载了几个类,这些类占用的内存大小
案例4 查看线程的执行信息
执行命令:jcmd <pid> Thread.print
,这个命令类似于jstack命令
jconsole 图形化监控工具
图形化界面,查看java程序的信息,包括内存使用率、CPU使用率、线程数、类加载情况等,还可以手动执行垃圾回收、检测死锁等。
使用方式:在命令行直接输入 jconsole 命令即可使用,它会自动检查当前机器上运行的jvm
启动界面:
监控界面:
使用案例
案例1 执行GC
案例2 检测死锁
如何连接到远程服务器上的进程?
需要进程本身支持远程连接,在进程启动时添加参数,例如:
java -jar -Dcom.sun.management.jmxremote.port=8899 \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false \
-Djava.rmi.server.hostname=192.168.0.3 \
math-game.jar
在进程启动时,添加jmx远程支持,指定进程本身的ip地址和端口号,然后就可以使用jconsole远程连接了
jvisualvm 图形化监控工具
图形化界面,查看java程序的信息,包括内存使用率、CPU使用率、线程数、类加载情况等,还可以手动执行垃圾回收、打印堆快照等。
这个命令是Java自带的,但是Java8在某些版本之后不支持这个软件了,需要自行下载。
下载地址:https://visualvm.github.io/download.html
使用方式:在命令行直接输入 jvisualvm 命令即可使用,它会自动检查当前机器上运行的jvm。
使用案例
案例1 查看堆转储文件
1、将堆转储文件加载到visualVM中。(这里mac上的图标,其它平台的可能不一样)
2、查看程序的统计信息
3、查看大对象:某些对象可以右击选择“select in threads”,查看它被哪个线程持有。在这个案例中,大对象是存储在ArrayList中的字节数组,可以在代码中搜索一下,找到这样的数组,然后再继续分析。
4、查看线程的执行信息,可以直接看到抛出内存溢出的位置,但是这未必是需要修复的位置,还是需要分析内存中的大对象是怎么来的,然后优化这部分对象。
5、QQL,一种查询语言,类似于SQL,可以查找某个类的所有实例。
第三方工具
arthus
arthas,阿里巴巴开源的Java诊断工具,用于诊断线上问题。
安装
下载arthas的jar包即可,curl -O https://arthas.aliyun.com/arthas-boot.jar
,
第一次启动arthas时,会下载arthas需要的库文件到用户根目录的 “.arthas” 文件中,arthas的日志文件在用户根目录的 “logs/arthas” 中
快速入门
通过一个实际案例来演示arthas的使用。
第一步:准备
- 下载arthus:
curl -O https://arthas.aliyun.com/arthas-boot.jar
- 下载arthus提供的用于演示的jar包:
curl -O https://arthas.aliyun.com/math-game.jar
第二步:启动arthas
- 启动演示jar包:
java -jar math-game.jar
,这个jar包中提供了一个小程序,用于计算某个随机数的可以被分解成哪些因数 - 启动arthus:
java -jar arthas-boot.jar
第三步:使用arthas来查看正在运行的Java项目
- 启动之后,进入arthas的交互界面,它会列出当前机器上运行的Java程序,用户选择一个进入,arthas会attach到该进程
- attach到目标进程后,可以执行的命令:
- daashboard:查看当前进程的详细信息
arthas命令
输入help命令,可以查看arthus提供了哪些命令。这里介绍了几个我用过的命令
jvm相关
监控面板 dashboard
当前系统的实时面板,默认5s刷新一次,按q退出。
面板中的数据:包括线程信息、内存信息、jvm信息
线程部分:
- ID:Java 级别的线程 ID
- NAME: 线程名
- GROUP: 线程组名
- PRIORITY: 线程优先级, 1到10之间的数字,越大表示优先级越高
- STATE: 线程的状态
- CPU%: 线程的cpu使用率。比如采样间隔1000ms,某个线程的增量cpu时间为100ms,则 cpu 使用率 =100/1000 = 10%
- DELTA_TIME: 上次采样之后线程运行增量CPU时间,数据格式为秒
- TIME: 线程运行总 CPU 时间,数据格式为分:秒
- INTERRUPTED: 线程当前的中断位状态
- DAEMON: 是否是daemon线程
堆转储文件 heapdump
类似 jmap 命令的 heap dump 功能
案例:heapdump arthas-output/dump.hprof
查看jvm信息 jvm
包括系统信息、类加载器信息、内存信息、线程信息的基本统计
查看内存信息 memory
堆内存中各个部分的使用信息
采样 生成火焰图 profiler
用于生成应用热点的火焰图。它通过不断采样,将收集到的调用链路生成火焰图,帮助开发者快速定位性能瓶颈
案例1:profiler start -e cpu -d 120,采样CPU使用情况,持续120秒,会生成一个html文件
案例2:
- 开始采样:profiler start -e cpu,采样事件为cpu
- 查看采样状态:profiler status
- 停止采样并生成结果:profiler stop,会生成一个html文件
火焰图应该如何分析:火焰图的Y轴表示调用栈,每一层都是一个函数;X轴表示采样数,宽度越宽表示该函数被采样的次数越多,即占用的CPU时间越长。在火焰图中,热点方法通常位于图的顶部,且宽度较宽。这些方法表示在采样期间占用CPU时间最多的函数
类加载器相关
类加载器 classloader
查看当前类加载器的信息
选项:
- -c:哈希值:指定当前classloader的哈希值,可以用于查看具体的classloader的信息
- -a:列出当前classloader加载的所有类
- -t:查看类加载器的继承树
- -l:列出更多信息
案例1:执行classloader命令
输出结果:
name numberOfInstances loadedCountTotal
BootstrapClassLoader 1 2358
com.taobao.arthas.agent.ArthasClassloader 1 1574
sun.misc.Launcher$ExtClassLoader 1 47
sun.reflect.DelegatingClassLoader 16 16
sun.misc.Launcher$AppClassLoader 1 4
Affect(row-cnt:5) cost in 6 ms.
结果中,numberOfInstances是类加载器的实例数量,loadedCountTotal是类加载器加载了多少个类
案例2:查看累加器的继承树
执行命令:classloader -t
输出结果:
+-BootstrapClassLoader
+-sun.misc.Launcher$ExtClassLoader@74a14482
+-com.taobao.arthas.agent.ArthasClassloader@4bead177
+-sun.misc.Launcher$AppClassLoader@55f96302
Affect(row-cnt:4) cost in 7 ms.
案例3:类加载器的哈希值
执行命令:classloader -l
输出结果:
name loadedCount hash parent
BootstrapClassLoader 2361 null null
com.taobao.arthas.agent.ArthasClassloader@4bead177 1580 4bead177 sun.misc.Launcher$ExtC
lassLoader@74a14482
sun.misc.Launcher$AppClassLoader@55f96302 4 55f96302 sun.misc.Launcher$ExtC
lassLoader@74a14482
sun.misc.Launcher$ExtClassLoader@74a14482 47 74a14482 null
案例4:查看classloader的加载路径
通过-c选项指定类加载器的哈希值
执行命令: classloader -c 74a14482,这里查看的是扩展类加载器的加载路径
输出结果:
file:/D:/java/jre/lib/ext/access-bridge-64.jar
file:/D:/java/jre/lib/ext/cldrdata.jar
file:/D:/java/jre/lib/ext/dnsns.jar
file:/D:/java/jre/lib/ext/jaccess.jar
file:/D:/java/jre/lib/ext/jfxrt.jar
file:/D:/java/jre/lib/ext/localedata.jar
file:/D:/java/jre/lib/ext/nashorn.jar
file:/D:/java/jre/lib/ext/sunec.jar
file:/D:/java/jre/lib/ext/sunjce_provider.jar
file:/D:/java/jre/lib/ext/sunmscapi.jar
file:/D:/java/jre/lib/ext/sunpkcs11.jar
file:/D:/java/jre/lib/ext/zipfs.jar
Affect(row-cnt:24) cost in 1 ms.
案例5:列出当前classloader加载的所有类
执行命令:classloader -c 55f96302 -a
返回结果:
hash:55f96302, sun.misc.Launcher$AppClassLoader@55f96302
com.taobao.arthas.agent.ArthasClassloader
com.taobao.arthas.agent334.AgentBootstrap
com.taobao.arthas.agent334.AgentBootstrap$1
demo.MathGame
反编译字节码 jad
反编译类的字节码
案例:jad 类的全限定名
方法相关
方法执行监控 monitor
监控方法的平均执行时间和失败率
格式:monitor [-c 执行周期] 类名 方法名
,执行周期默认是120s
案例:monitor -c 5 demo.MathGame print
函数执行数据观测 watch
监控方法的执行时间和返回结果
格式:watch 类的全限定名 方法名
案例:watch demo.MathGame print
MAT
Eclipse Memory Analyzer Tool,JVM堆内存离线分析工具,基于eclipse开发,用于分析堆转储文件。
MAT提供了称为支配树(dominator tree)的对象图,支配树展示了对象实例之间的引用关系。
浅堆和深堆:
- 浅堆:shallow heap,支配树中对象本身占用的空间称为浅堆
- 深堆:retained heap,支配树中一个对象和它的子树称为深堆,深堆表示如果对象被回收掉,可以释放多少内存
MAT根据支配树,从叶子节点向根节点遍历,发现如果深堆的大小超过一定阈值比例,就会将其标记成为内存泄漏的嫌疑对象。
对象的引用:
- outgoing reference:查看对象所引用的对象
- incoming reference:查看对象被哪些对象引用
MAT提供的功能:
- Overview:快速查看内存使用情况。
- Histogram:柱状图,查看对象的分布情况,包括实例数量和内存占用。
- Dominator Tree:支配树,查找占用最多内存的对象及其引用路径。
操作界面:
MAT会查看对象间的引用信息,分析可能存在的内存泄漏点,然后给出分析报告
常用操作
查看jvm的运行情况
查看jvm的启动参数
方法一:jps命令,使用不同的选项,-v 显示传递给jvm的实参,-m 显示传递给main方法的实参
方法二:jinfo -flags 进程id
案例:
参数讲解:
- -XX:CICompilerCount=2 :设置编译器线程的数量
- -XX:InitialHeapSize=25165824 :设置 JVM 启动时的初始堆大小, 24M
- -XX:MaxHeapSize=383778816 :设置 JVM 的最大堆大小
- -XX:MaxNewSize=127926272 :设置新生代的最大大小
- -XX:MinHeapDeltaBytes=196608 :设置堆内存扩展的最小增量,当 JVM 需要扩展堆内存时,每次扩展的最小增量。此参数可以避免频繁的内存扩展操作,提高性能。
- -XX:NewSize=8388608 :设置新生代的初始大小
- -XX:OldSize=16777216 :设置老年代的初始大小
- -XX:+UseCompressedClassPointers :启用压缩类指针,在 64 位 JVM 中,启用压缩类指针可以减少内存占用,提高性能。此参数在 Java 8 及更高版本中默认启用
- -XX:+UseCompressedOops :启用压缩普通对象指针
查看jvm中线程的执行信息
jstack 进程id
查看jvm中堆内存的统计信息
jmap -heap 进程id
查看jvm堆中对象的统计信息
jmap -histo 进程id
查看jvm使用哪个垃圾收集器
Java8默认使用吞吐量优先的垃圾收集器。通过查看jvm的启动参数 jinfo -flags 进程id
来查看,如果没有明确指定,那就是吞吐量优先的垃圾收集器,也就是Parallel GC。
查看jvm的gc信息
gc统计信息
jstat -gc ${pid}
gc日志
在程序启动时添加参数,把gc日志打印到指定文件中
java -XX:+PrintGCDetails -Xloggc:./gc.log -jar math-game.jar
查看类加载信息
如何统计类的加载信息?详细统计一个运行中的程序,哪个类被哪个类加载器加载
Jstat命令:统计类加载信息,无法具体到类加载器
jcmd命令:统计类加载器的信息,无法统计到具体的类
arthus:可以列出某个类加载器加载的所有类,可以反编译某个类,
- 第一步:classloader -l ,查看类加载器的哈希值
- 第二步:classloader -c 哈希值 -a,查看指定类加载器加载的所有类
如果发生内存溢出 打印堆转储文件
在程序启动时添加jvm参数:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dumpfile_$(date +%Y%m%d_%H%M%S).hprof
如果启动时没有添加这个参数,那么即使发生内存溢出,也不会打印堆转储文件
踩坑记录
在mac上无法使用jinfo命令
问题:在macOs上无法使用jinfo命令,执行时报错,无法attach到进程上,我使用的jdk版本是 “1.8.0_411”,
原因:mac上jdk的小版本bug,需要升级jdk。参考这篇博客 https://www.jianshu.com/p/d30cc106894d
在mac上无法使用jmap命令
问题:在macOs上无法使用jmap命令,执行时报错,无法attach到进程上,我使用的jdk版本是 “1.8.0_411”,
原因:网上找方案,发现要升级jdk,这是java8在macOS上的一个bug,参考官网记录 https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8160376
在mac上启动jvisualvm时报错
报错信息:
The operation couldn’t be completed. Unable to locate a Java Runtime that supports jvisualvm.
Please visit http://www.java.com for information on installing Java.
原因是,jdk 1.8.0_361之后需要自行下载安装VisualVM,前往 https://visualvm.github.io 下载。参考这篇博客:https://blog.csdn.net/weixin_43859011/article/details/132805006
在mac上visualWM无法链接到java程序
点击Applications,就会出现本地进程和远程进程的对话框,可以选择某个进程。 // 这个图标确实不好找
在mac m系列芯片上安装MAT
MAT最新版需要使用Java17,我以为它不支持解析Java8的堆转储文件,但试了一下是支持的。所以先安装Java17、再安装MAT即可。
mat官网:https://eclipse.dev/mat/
历史版本下载页面:https://eclipse.dev/mat/download/previous/
参考
- MAT的使用:https://juejin.cn/post/6911624328472133646
- https://www.jianshu.com/p/f04c04ed462f