本文对Java虚拟机工具的使用进行总结,并且记录了通过堆栈快照进行排查问题的思路,以便后续再发生问题时能够提供参考。
一、Java虚拟机工具
1、jps命令
jps是java提供的一个显示当前所有java进程pid的命令,适合在linux/unix平台上简单察看当前java进程的一些简单情况。很多人都是用过unix系统里的ps命令,这个命令主要是用来显示当前系统的进程情况,有哪些进程以及进程id。 jps 也是一样,它的作用是显示当前系统的java进程情况及进程id。
usage: jps [-help]
jps [-q] [-mlvV] [<hostid>]
Definitions:
<hostid>: <hostname>[:<port>]
说明:
-q 只显示pid,不显示class名称,jar文件名和传递给main方法的参数
-m 输出传递给main方法的参数,在嵌入式jvm上可能是null
-l 输出应用程序main class的完整package名或者应用程序的jar文件完整路径名
-v 输出传递给JVM的参数
-V 隐藏输出传递给JVM的参数
示例1:只使用jps命令
示例2:jps -q
示例3: jps -m
示例4:jps -l
示例5: jps -v
jps -v 可以用于查看JVM的启动参数
jps主要是为了获取java进程的PID,以及使用jps -v查看传递给JVM的参数。
2、jmap 命令
命令jmap是一个多功能的命令。它可以生成 java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列。
Usage:
jmap [option] <pid>
(to connect to running process)
jmap [option] <executable <core>
(to connect to a core file)
jmap [option] [server_id@]<remote server IP or hostname>
(to connect to remote debug server)
where <option> is one of:
<none> 查看进程的内存映像信息,类似 Solaris pmap 命令。
-heap 显示Java堆详细信息
-histo[:live] 显示堆中对象的统计信息
-clstats 打印类加载器信息
-finalizerinfo 显示在F-Queue队列等待Finalizer线程执行finalizer方法的对象
-dump:<dump-options> 生成堆转储快照
dump-options:
live 只dump出存活的对象,如果不指定,堆中所有对象都会被dump
format=b binary format
file=<file> dump heap to <file>
Example: jmap -dump:live,format=b,file=heap.bin <pid>
-F force. Use with -dump:<dump-options> <pid> or -histo
to force a heap dump or histogram when <pid> does not
respond. The "live" suboption is not supported
in this mode.
-h | -help to print this help message
-J<flag> 指定传递给运行jmap的JVM的参数
示例1:no option
命令:jmap pid
使用不带选项参数的jmap打印共享对象映射,将会打印目标虚拟机中加载的每个共享对象的起始地址、映射大小以及共享对象文件的路径全称。这与Solaris的pmap工具比较相似。
示例2:heap
命令:jmap -heap pid
显示Java堆详细信息,打印一个堆的摘要信息,包括使用的GC算法、堆配置信息和各内存区域内存使用信息。
示例3:histo[:live]
命令:jmap -histo:live pid
显示堆中对象的统计信息。其中包括每个Java类、对象数量、内存大小(单位:字节)、完全限定的类名。打印的虚拟机内部的类名称将会带有一个’*’前缀。如果指定了live子选项,则只计算活动的对象。
示例4:clstats
命令:jmap -clstats pid
-clstats是-permstat的替代方案,在JDK8之前,-permstat用来打印类加载器的数据
打印Java堆内存的永久保存区域的类加载器的智能统计信息。对于每个类加载器而言,它的名称、活跃度、地址、父类加载器、它所加载的类的数量和大小都会被打印。此外,包含的字符串数量和大小也会被打印。
示例5:finalizerinfo
命令:jmap -finalizerinfo pid
描述:打印等待终结的对象信息
Number of objects pending for finalization: 0 说明当前F-QUEUE队列中并没有等待Fializer线程执行final
示例6:dump:
命令:jmap -dump:format=b,file=heapdump.hprof pid
生成堆转储快照dump文件。以hprof二进制格式转储Java堆到指定filename的文件中。live子选项是可选的。如果指定了live子选项,堆中只有活动的对象会被转储。想要浏览heap dump,你可以使用jhat(Java堆分析工具)读取生成的文件。
这个命令执行,JVM会将整个heap的信息dump写入到一个文件,heap如果比较大的话,就会导致这个过程比较耗时,并且执行的过程中为了保证dump的信息是可靠的,所以会暂停应用, 线上系统慎用。
jmap -dump:live,format=b,file=heap.bin
3、jstat命令
Jstat是JDK自带的一个轻量级小工具。全称“Java Virtual Machine statistics monitoring tool”,它位于java的bin目录下,主要利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收状况的监控。
Usage: jstat -help|-options
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
option: 参数选项
-t: 可以在打印的列加上Timestamp列,用于显示系统运行的时间
-h: 可以在周期性数据数据的时候,可以在指定输出多少行以后输出一次表头
vmid: Virtual Machine ID( 进程的 pid)
interval: 执行每次的间隔时间,单位为毫秒
count: 用于指定输出多少次记录,缺省则会一直打印
option 可以从下面参数中选择
-class 显示ClassLoad的相关信息;
-compiler 显示JIT编译的相关信息;
-gc 显示和gc相关的堆信息;
-gccapacity 显示各个代的容量以及使用情况;
-gcmetacapacity 显示metaspace的大小
-gcnew 显示新生代信息;
-gcnewcapacity 显示新生代大小和使用情况;
-gcold 显示老年代和永久代的信息;
-gcoldcapacity 显示老年代的大小;
-gcutil 显示垃圾收集信息;
-gccause 显示垃圾回收的相关信息(通-gcutil),同时显示最后一次或当前正在发生的垃圾回收的诱因;
-printcompilation 输出JIT编译的方法信息;
示例1:-class
显示加载class的数量,及所占空间等信息。
- Loaded : 已经装载的类的数量
- Bytes : 装载类所占用的字节数
- Unloaded:已经卸载类的数量
- Bytes:卸载类的字节数
- Time:装载和卸载类所花费的时间
示例2:-compiler
显示VM实时编译(JIT)的数量等信息。
示例3:-gc
显示和gc相关的堆信息
示例4:-gcmetacapacity
显示metaspace的大小
4、jstack 命令
Jstack是JVM自带dump线程堆栈的工具,很轻量易用,并且执行时不会对性能造成很大的影响。灵活的使用jstack可以发现很多隐秘的性能问题,是定位问题不可多得的好帮手。线程堆栈也称作线程调用堆栈。Java线程堆栈是虚拟机中线程(包括锁)状态的一个瞬间快照,即系统在某个时刻所有线程的运行状态,包括每一个线程的调用堆栈,锁的持有情况等信息,从线程堆栈中可以得到以下信息:
- 线程的名字,ID,线程的数量等;
- 线程的运行状态,锁的状态(锁被那个线程持有,哪个线程在等待锁等);
- 函数间的调用关系,包括完整类名,所执行的方法,源代码的行数等;
可以通过Jstack获取应用运行时的线程堆栈,可以通过如下方式获取线程堆栈:jstack pid > jstack.log。对于Java应用而言,一下常见的几个性能问题都可以从线程堆栈入手定位:
- 系统挂起无响应
- 系统CPU较高
- 系统运行的响应时间长
- 线程死锁等
实战1、 找出cpu占用最高的线程
1、首先查看CPU的使用情况,使用top命令查看cpu占用情况,排在第一位的是进程号为30328的进程,占用了6.6%的cpu;
2、找线程,知道进程号了,接着就是找线程了,输入以下命令
top -Hp 30328
打印结果如下,这里有一点需要注意,在我们加上-Hp指令后,PID展示就是线程的id了,这时候我们看到占用CPU最高的线程id是30365;
还有另一种方式,就是使用ps命令来查找线程
ps -mp 30328 -o THREAD,tid,time| sort -n -k1 -r
以上的方式我们成功找到了占用cpu高的线程id是11631,但这个id是十进制的,在这里需要先转为16进制,输入命令
printf "%x\n" 30365
3、使用jstack分析堆栈快照
jstack 30328 | grep '线程id' -A 50
经过查看就知道是哪个线程出了问题
二、一次生产环境的内存泄露问题
晚上吃过饭,正准备写会儿代码,清理一下本周的任务,突然报警里面报出许多慢请求日志,顿时感觉到不妙,马上查看监控发现内存快慢了,快撑不住了,情况紧急马上进入到容器的控制台,堆内存使用情况,果然老年代已经99%了,想到或许有内存泄露了,导致GC不掉日志。
想到这里,一个电话打到运维哪里,赶紧先重启其他服务,留下一台dump出内存转储快照。运维也是很给力一会就把内存文件整出来了,使用MAT费了半天劲打开发现,一大部分已经被占据
然后点开详情信息,发现QueryPlanCache这个类对象竟然占据了90%多的内存,看这类的名字,难道是缓存的数据吗?
查了一下问题原因:
hibernate中的QueryPlanCache会缓存sql,以便于后边的相同的sql重复编译。如果in后的参数不同,hibernate会把其当成不同的sql进行缓存,从而缓存大量的sql导致heap内存溢出。
最新上线的服务刚刚进行完springboot改造,服务中还是有不少地方用到了in的查询的,这些数据被大量缓存导致了内存溢出,通过设置缓存最大值来进行限制,不设置默认是2G。
spring:
jpa:
properties:
hibernate:
query:
plan_cache_max_size: 64
plan_parameter_metadata_max_size: 32
plan_cache_max_soft_references: 1024
plan_cache_max_strong_references: 64
三、总结
生产问题不要急,冷静分析才可以,堆栈日志要查起,及时总结才可以。