前言
工程学科是在不断动手的过程中来细化自己的领域,就像练武功一样,单凭着掌握一身武林绝学是远远不够的,在实战中,如何在多变的环境面前使用合适的招式,怎样出击,应该选择怎样的功法去迎击未知的对手,这往往都是经验之谈,当你踩过了无数的坑,沉淀了无数的线上问题,总结无数的经验,当问题来的时候你能拥有自己独特的见解与方案,你才能所向披靡,战无不胜。
当你经验少的时候,要多学会辛苦一下自己,大不了就是比别人多干点活,通过不断编码,不断试错,你才能熟能生巧,慢慢的,我们才会从一个以苦劳展现价值的工程师,发展成一名靠经验与绝学创造价值的架构师。
当然,熟练掌握所有的武林招式与绝学,是参与实战的前提,我们还要多沉淀一些线上的问题和处理问题的经验,多阅读一些经典的书籍和框架的源码,努力提升自己的抽象思维与架构设计能力,日积月累,我们才能预见更好的自己,达到更高的高度。长路漫漫,我们巅峰相见。
JVM调优常见工具
这次我们主要聊一聊JVM调优圈子里面比较主流的监控类工具、故障排查工具以及可视化工具
我们这篇文章主要围绕着JDK11 进行介绍,这是因为JDK11的一些工具要比JDK8多一些。比方说jhsdb 是JDK9 推出的,在JDK8是没有的,特别要注意的是,在Mac版的JDK中,有一些工具是无法使用的,比如jinfo.
涵盖工具
JDK内置工具
内置工具包括了JDK提供的常用监控工具以及故障排查工具,主要包括了:
监控工具,包括:
- jps
- jstat
故障排查工具,包括:
- jinfo
- jmap
- jstack
- jcmd
- jhat
- jhsdb
可视化工具,包括:
- jhsdb
- jconsole
- VisualVM
- Java Mission Control
这些工具从可用性以及授权的不同,主要可以分为三类:
- 正式支持工具:表示这类工具会有长期技术支持,不同的平台、不同的JDK版本之间,这些工具可能会有一定的差异,但总体来说还是比较兼容的。
- 实验性工具:这类工具会被声明是实验性质,不会有技术支持,一些工具甚至可能在某个新的JDK版本中突然就消失了。不过这类命令其实也都非常稳定,而且功能很强大,也是可以用在生产的。在实际项目中定位问题发挥的作用也非常大,所以千万不要一看某个工具是实验性的就不学了。
- 商业授权工具:指的主要是JMC以及JMC需要用到的JFR,这些工具在商业环境中使用的话是要付费的,但是一般来说在个人开发环境中使用是免费的。
第三方工具
- Memory Analyzer Tool
- JITWatch
当然除本系列列出的工具外,还有很多其他的工具,比如JProfiler等,但由于其收费的特性,故而本系列不去探讨。感兴趣可以自行研究,使用起来也都不难。
我们接下来就来探讨一下这些工具的使用吧。
监控工具
JPS
jps全称为 Java Virtual Machine Process Status Tool,主要用来查看JVM进程状态。
此命令是实验性的,不受支持
它虽然是一款实验性的工具,但是实际项目中应用的非常多。
来玩一下:
我们可以看到,他能输出所有JVM进程。这个工具还有很多的参数:
使用说明
命令如下:
-> jps -h
illegal argument: -h
usage: jps [-help]
jps [-q] [-mlvV] [<hostid>]
Definitions:
<hostid>: <hostname>[:<port>]
参数如下:
-q 只显示进程号
-m 显示传递给main方法的参数
-l 显示应用main class的完整包应用的jar文件完整路径名
-v 显示传递给JVM的参数
-V 禁止输出类名、JAR文件名和传递给main方法的参数,仅显示本地JVM标识符的列表。
当然,你也可以多个参数配合使用,比如:
这样既可以展示传递给main方法的参数,也可以展示启动类的完整包名。
jstat
jstat全称JVM Statistics Monitoring Tool,用于监控JVM的各种运行状态。
该命令为实验性,不受支持。
它也是一个实验性工具,实际项目中也非常常用。
使用说明:
<option> 指定参数,取值可用jstat -options查看
<vmid> VM标识,格式为<lvmid>[@<hostname>[:<port>]
<lvmid>:如果lvmid是本地VM,那么用进程号即可;
<hostname>:目标JVM的主机名;
<port>:目标主机的rmiregistry端口;
-t 用来展示每次采样花费的时间
-h<lines> 每抽样几次就列一个标题,默认0,显示数据第一行的列标题
<interval> 抽样的周期,格式使用:<n>["ms"|"s"],n是数字,ms/s是时间单位,默认是ms
<count> 采样多少次停止
我们也来玩一下,首先我们先用Jps看一下进程号有哪些。
比如我想监控aym-api-2.0.jar这个进程,第一个参数需要传一个option
option取值:
- class:显示类加载器的统计信息
- compliler:显示有关Java HotSpot VM即时编译器行为的统计信息
- gc:显示有关垃圾收集堆行为的统计信息
- gccapacity:统计各个分代(新生代,老年代,持久代)的容量情况
- gccause:显示引起垃圾收集事件的原因
- gcnew:显示有关新生代行为的统计信息
- gcnewcapacity:显示新生代容量
- gcold:显示老年代、元空间的统计信息
- gcoldcapacity:显示老年代的容量
- gcmetacapacity:显示元空间的容量
- gcuil:显示有关垃圾收集统计信息的摘要
- printcompilation:显示java HotSpot VM编译方法统计信息
紧接着,后面需要传一个[-t]
的可选参数,展示采样的花费时间。
然后还需要-h<lines>
的参数,比如我们传一个-h3 就表示每采样3次后就输出一次列标题
再之后<vmid>
这是一个进程的唯一标识,对于本地的进程就是进程号,对于远程机器上的进程需要<lvmid>[@<hostname>[:<port>]
这种格式编写
再之后<interval>
每隔多久监控一次(单位毫秒)
最后<count>
表示监控几次后退出
如下:
输出信息
这里我梳理了一些当option对应取值时,相关的输出信息含义。
-
class:
- Loaded:当前加载的类的数量
- Bytes:当前加载的空间(单位KB)
- Unloaded:卸载的类的数量Number of classes unloaded
- Bytes:当前卸载的空间(单位KB)
- Time:执行类加载/卸载操作所花费的时间
-
compiler:
- Complied:执行了多少次编译任务
- Failed:多少次编译任务执行失败
- Invalid:无效的编译任务数
- Time:执行编译任务所花费的时间
- FailedType:上次失败的编译的编译类型
- FailedMethod:上次失败的编译的类名和方法
-
gc
- S0C:第一个存活区(S0)的容量(KB)
- S1C:第二个存活区(S1)的容量
- S0U:第一个存活区(S0)使用的大小
- S1U:第二个存活区(S1)使用的大小
- EC:伊甸园空间容量
- EU:伊甸园使用的大小
- OC:老年代容量
- OU:老年代使用的大小
- MC:元空间的大小
- MU:元空间使用的大小
- CCSC:压缩的类空间大小
- CCSU:压缩类空间使用的大小
- YGC:年轻代垃圾收集事件的数量
故障排查工具
jinfo
jinfo全称JAVA Configuration Info ,主要用于查看与调整JVM参数
此命令是实验性的,不受支持,对于JDK9 及更高版本,部分功能可使用
jhsdb
代替,或者jcmd
代替。
部分JDK版本的jinfo命令对Windows支持比较有限,参数较少。文章为了更加接近生产环境,都是基于Linux操作系统编写。如果在Windows操作系统下测试,应以
jinfo -h
的结果为准。
我们前面聊过,通过jps -v
是可以查看JVM启动的时候所指定的参数,但是如果想动态的查看那些并没有显式指定的参数的默认值呢,这个时候jinfo 就发挥作用了,我们来看下jinfo是怎么玩的。
-flag <name> 打印指定参数的值
-flag [+|-]<name> 启用/关闭指定参数
-flag <name>=<value> 将指定的参数设置为指定的值
-flags 打印VM参数
-sysprops 打印系统属性(系统打印的是System.getProperties()的结果)
<no option> 打印VM参数及系统属性
使用示例
查看参数
示例1: 打印12737这个进程的VM参数及Java系统属性:
jinfo 12737
结果如下:
Attaching to process ID 12737, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.131-b11
# 系统属性 System.getProperties()结果
Java System Properties:
rocketmq.client.logFileMaxSize = 67108864
java.runtime.name = Java(TM) SE Runtime Environment
java.vm.version = 25.131-b11
sun.boot.library.path = /usr/java/jdk1.8.0_131/jre/lib/amd64
java.protocol.handler.pkgs = org.springframework.boot.loader
rocketmq.remoting.version = 282
java.vendor.url = http://java.oracle.com/
java.vm.vendor = Oracle Corporation
path.separator = :
file.encoding.pkg = sun.io
java.vm.name = Java HotSpot(TM) 64-Bit Server VM
sun.os.patch.level = unknown
sun.java.launcher = SUN_STANDARD
user.country = US
user.dir = /home/server/release
java.vm.specification.name = Java Virtual Machine Specification
PID = 12737
java.runtime.version = 1.8.0_131-b11
java.awt.graphicsenv = sun.awt.X11GraphicsEnvironment
rocketmq.client.logLevel = INFO
os.arch = amd64
java.endorsed.dirs = /usr/java/jdk1.8.0_131/jre/lib/endorsed
line.separator =
java.io.tmpdir = /tmp
java.vm.specification.vendor = Oracle Corporation
os.name = Linux
rocketmq.client.logFileMaxIndex = 10
rocketmq.client.logFileName = ons.log
sun.jnu.encoding = UTF-8
java.library.path = /usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib
spring.beaninfo.ignore = true
sun.nio.ch.bugLevel =
java.specification.name = Java Platform API Specification
java.class.version = 52.0
sun.management.compiler = HotSpot 64-Bit Tiered Compilers
os.version = 4.19.91-23.al7.x86_64
user.home = /home/server
user.timezone = Asia/Shanghai
catalina.useNaming = false
java.awt.printerjob = sun.print.PSPrinterJob
file.encoding = UTF-8
rocketmq.client.logRoot = /home/server/logs
java.specification.version = 1.8
catalina.home = /tmp/tomcat.8902966359126221197.8001
user.name = server
java.class.path = /home/server/release/current_dev/aym-app-api-2.0.jar
java.vm.specification.version = 1.8
sun.arch.data.model = 64
sun.java.command = /home/server/release/current_dev/aym-app-api-2.0.jar --spring.profiles.active=dev
java.home = /usr/java/jdk1.8.0_131/jre
user.language = en
java.specification.vendor = Oracle Corporation
awt.toolkit = sun.awt.X11.XToolkit
java.vm.info = mixed mode
java.version = 1.8.0_131
java.ext.dirs = /usr/java/jdk1.8.0_131/jre/lib/ext:/usr/java/packages/lib/ext
sun.boot.class.path = /usr/java/jdk1.8.0_131/jre/lib/resources.jar:/usr/java/jdk1.8.0_131/jre/lib/rt.jar:/usr/java/jdk1.8.0_131/jre/lib/sunrsasign.jar:/usr/java/jdk1.8.0_131/jre/lib/jsse.jar:/usr/java/jdk1.8.0_131/jre/lib/jce.jar:/usr/java/jdk1.8.0_131/jre/lib/charsets.jar:/usr/java/jdk1.8.0_131/jre/lib/jfr.jar:/usr/java/jdk1.8.0_131/jre/classes
java.awt.headless = true
java.vendor = Oracle Corporation
catalina.base = /tmp/tomcat.8902966359126221197.8001
file.separator = /
java.vendor.url.bug = http://bugreport.sun.com/bugreport/
sun.io.unicode.encoding = UnicodeLittle
sun.cpu.endian = little
sun.cpu.isalist =
# JVM参数
VM Flags:
Non-default VM flags: -XX:CICompilerCount=2 -XX:InitialHeapSize=125829120 -XX:MaxHeapSize=1996488704 -XX:MaxNewSize=665321472 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=41943040 -XX:OldSize=83886080 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseParallelGC
Command line:
我们可以从输出结果中看出,一共分为两部分,第一部分Java System Properties是系统属性,相当于System.getProperties()打印的结果。
第二部分为JVM的一些参数,从里面我们可以得到很多信息,比如各个区域内存大小,采用了什么垃圾收集器…
示例2:打印12737这个进程的java系统属性
jinfo -sysprops 12737
Attaching to process ID 12737, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.131-b11
rocketmq.client.logFileMaxSize = 67108864
java.runtime.name = Java(TM) SE Runtime Environment
java.vm.version = 25.131-b11
sun.boot.library.path = /usr/java/jdk1.8.0_131/jre/lib/amd64
java.protocol.handler.pkgs = org.springframework.boot.loader
rocketmq.remoting.version = 282
java.vendor.url = http://java.oracle.com/
java.vm.vendor = Oracle Corporation
path.separator = :
file.encoding.pkg = sun.io
java.vm.name = Java HotSpot(TM) 64-Bit Server VM
sun.os.patch.level = unknown
sun.java.launcher = SUN_STANDARD
user.country = US
user.dir = /home/server/release
java.vm.specification.name = Java Virtual Machine Specification
PID = 12737
java.runtime.version = 1.8.0_131-b11
java.awt.graphicsenv = sun.awt.X11GraphicsEnvironment
rocketmq.client.logLevel = INFO
os.arch = amd64
java.endorsed.dirs = /usr/java/jdk1.8.0_131/jre/lib/endorsed
line.separator =
java.io.tmpdir = /tmp
java.vm.specification.vendor = Oracle Corporation
os.name = Linux
rocketmq.client.logFileMaxIndex = 10
rocketmq.client.logFileName = ons.log
sun.jnu.encoding = UTF-8
java.library.path = /usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib
spring.beaninfo.ignore = true
sun.nio.ch.bugLevel =
java.specification.name = Java Platform API Specification
java.class.version = 52.0
sun.management.compiler = HotSpot 64-Bit Tiered Compilers
os.version = 4.19.91-23.al7.x86_64
user.home = /home/server
user.timezone = Asia/Shanghai
catalina.useNaming = false
java.awt.printerjob = sun.print.PSPrinterJob
file.encoding = UTF-8
rocketmq.client.logRoot = /home/server/logs
java.specification.version = 1.8
catalina.home = /tmp/tomcat.8902966359126221197.8001
user.name = server
java.class.path = /home/server/release/current_dev/aym-app-api-2.0.jar
java.vm.specification.version = 1.8
sun.arch.data.model = 64
sun.java.command = /home/server/release/current_dev/aym-app-api-2.0.jar --spring.profiles.active=dev
java.home = /usr/java/jdk1.8.0_131/jre
user.language = en
java.specification.vendor = Oracle Corporation
awt.toolkit = sun.awt.X11.XToolkit
java.vm.info = mixed mode
java.version = 1.8.0_131
java.ext.dirs = /usr/java/jdk1.8.0_131/jre/lib/ext:/usr/java/packages/lib/ext
sun.boot.class.path = /usr/java/jdk1.8.0_131/jre/lib/resources.jar:/usr/java/jdk1.8.0_131/jre/lib/rt.jar:/usr/java/jdk1.8.0_131/jre/lib/sunrsasign.jar:/usr/java/jdk1.8.0_131/jre/lib/jsse.jar:/usr/java/jdk1.8.0_131/jre/lib/jce.jar:/usr/java/jdk1.8.0_131/jre/lib/charsets.jar:/usr/java/jdk1.8.0_131/jre/lib/jfr.jar:/usr/java/jdk1.8.0_131/jre/classes
java.awt.headless = true
java.vendor = Oracle Corporation
catalina.base = /tmp/tomcat.8902966359126221197.8001
file.separator = /
java.vendor.url.bug = http://bugreport.sun.com/bugreport/
sun.io.unicode.encoding = UnicodeLittle
sun.cpu.endian = little
sun.cpu.isalist =
[server@server-test ~]$
通过指定-sysprops
这样就可以不打印JVM参数了
当然,你也可以使用-flags
只打印JVM参数。
再来看下-flag
的玩法:
-flag
玩法有好多种,比如我们可以通过指定一个名称,打印具体参数的值,比如查看UseParallelGC
的值
-flag
还有一种场景,比如查看通过-flags
并没有呈现出来的一些参数,比如我们查看线程栈的大小,输出里并没有这样的参数,我们也是可以玩的:
可以看大线程栈的大小是1M
拓展
要想查看JVM参数,也可以在启动时,指定-XX:+PrintFlagsFinal
,这样会在启动时将JVM参数打印到日志。
动态修改参数
jinfo还提供了动态修改参数的能力。
但是并不是所有的参数都可以动态修改,如果操作了不支持修改的参数将会报类似如下的异常
java Exception in thread "main" com.sun.tools.attach.AttachOperationFailedException: flag 'XXX' cannot be changed
使用如下命令展示出来的参数,基本都是可以支持动态修改的:
java -XX:+PrintFlagsInitial | grep manageable
看下输出的结果:
如果类型为bool类型是,我们修改的话需要在属性前以±号修改,+为true,-为false;如果值是数字,那么我们要以key = value 的形式修改
jmap
jmap全称Java Memory Map,用来展示对象内存映射或堆内存详细信息。
此命令是实验性的,不受支持,对于JDK9及更高版本,部分功能可使用
jhsdb jmap
代替,也可用jcmd代替。
部分JDK版本的jmap命令对Windows支持也比较有限,参数较少。如果在Windows下测试,应以jmap -h结果为准
命令格式是这样的:
jmap [options] pid
其中 options可选项如下:
- -clstats:连接到正在运行的进程,并打印Java堆的类加载器统计信息
- -finalizerinfo:连接到正在运行的进程,并打印等待finalization的对象的信息
- -histo[:live]:连接到正在运行的进程,并打印Java堆的直方图。如果制定了live子选项,则仅统计活动对象
- -dump:dump_options:连接到正在运行的进程,并转储Java堆,其中,dump_options的取值为:
- live:指定时,仅Dump活动对象;如果未指定,则转储堆中的所有对象
- format=b:以hprof格式Dump堆
- file=filename:将堆Dump到filename
我们重点介绍一下,dump这个命令,也是最为重要的命令,它可以实现转储java堆,所谓转储java堆就是把java堆里面的对象作为一个快照,然后存储到一个文件里,这个文件也叫做堆dump或者堆转储文件,dump这个命令有三个子命令。我们来玩一下:
执行完后,就会在当前目录下找到一个mydump.hprof的文件。这个文件里存储的是12737这个进程当前堆内存里的快照。
不难发现jmap 这个命令,可以让我们了解到JVM的堆内存的一些信息。还可以生成堆内存的快照,这也是我们实际项目中排查内存分配问题的一款利器。
要想获取Java堆Dump,除了使用jmap外,还有以下方法:
- 使用
-XX:+HeapDumpOnOutOfMemoryError
,让虚拟机在OOM异常出现后自动生成Dump文件;- 使用
-XX:+HeapDumpOnCtrlBreak
,可使用[Ctrl]+[Break],让虚拟机生成Dump文件;- 在Linux操作系统下,发送
kill -3 pid
命令;
对于SpringBoot应用,也可以使用SpringBootActuator提供的/actuator/heapdump实现堆Dump。
jstack
jstack,全称Stack Trace for Java,用于打印当前虚拟机的线程快照(线程快照也叫Thread Dump或者javacore文件)
可以从线程快照里面看到每个线程到底在做什么事情。
提到这里,可以看出它和jmap的功能是差不多的,jmap可以实现对堆内存的dump,也就是把堆内存的快照存储起来,而jstack可以对线程进行dump,也就是把线程的快照存储起来,我们可以把这两者关联起来进行学习。
次命令也是实验性的,不受支持,部分功能可用
jhsdb jstack
代替
不同版本参数不同(JDK8 有-m、-F参数,JDK11没有了)
jstack的参数也比较简单
Options:
-l 显示有关锁的额外信息
-e 展示有关线程的额外信息(比如分配了多少内存、定义了多少个类等等)
来玩一下:
从这里面可看出,每个线程正在干什么,代码执行到了哪里
总结
最后,我们简单总结一些jstack
在实际项目中,如果你的应用出现长时间等待的时候,可以考虑使用jstack,比方说:线程死锁、死循环、远程请求长时间得不到返回,都可能会出现线程长时间的等待,那么使用jstack可以得到每个线程的调用信息,这样就可以知道那些没有响应的线程在后台到底做什么或者正在等待什么样的资源。
jhat
jhat(JVM Heap Analysis Tool)用来分析jmap生成的对Dump
此命令是实验性的,不受支持
jhat 功能不是很强,VisualVM,Eclipse Memory Analyzer等都比jhat强大,建议优先使用jhat的替代工具
并且在JDK11里已经被废弃
这里我们就不详细探讨jhat了,感兴趣的可以自己研究一下:
命令格式:
jhat [options] heap-dump-file
使用示例:
# 分析1.hprof,并开启对象分配调用栈的分析
jhat -stack true 1.hprof
# 分析1.hprof,开启对象分配调用栈的分析,关闭对象引用的分析
jhat -stack true -refs false 1.hprof
稍后片刻访问7000端口查看。