1 概述
1.1 性能优化步骤
1.1.1 第一步-性能监控
一种以非强行或者入侵方式收集或查看应用运营性能数据的活动。
监控通常是指一种在生产、质量评估或者开发环境下实施的带有预防或主动性的活动。
当应用相关干系人提出性能问题却没有提供足够多的线索时,首先我们需要进行性能监控,随后是性能分析。
1.1.2 第二步-性能分析
一种以侵入方式收集运行性能数据的活动,它会影响应用的吞吐量或响应性。
性能分析是针对性能问题的答复结果,关注的范围通常比性能监控更加集中。
性能分析很少在生产环境下进行,通常是在质量评估、系统测试或者开发环境下进行,是性能监控之后的步骤。
1.1.3 第三步-性能优化
一种为改善应用响应性或吞吐量而更改参数、源代码、属性配置的活动,性能调优是在性能监控、性能分析之后的活动。
1.2 性能指标
1.2.1 停顿时间(或响应时间)
提交请求和返回该请求的响应之间使用的时间,一般比较关注平均响应时间
常用操作的响应时间列表:
在垃圾回收环节中:
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
-XX:MaxGCPauseMillis 用于设置JVM垃圾回收中最大的暂停时间(STW:stop the world)
1.2.2 吞吐量
见概览
1.2.3 并发数
1000个人同时在线,估计并发数在5%-15%之间,也就是同时并发量:50 - 150之间。
1.2.4 内存占用
主要是堆空间占用多,元空间占少数
1.2.5 相互间的关系
以高速公路同行状况为例
吞吐量:每天通过高速公路收费站的车辆的数据(也可以理解为收费站收取的高速费)
并发数:高速公路上正在行驶的车辆的数目
响应时间:车速
2 JVM监控及诊断工具-命令行篇
2.1 概述
性能诊断是软件工程师在日常工作中需要经常面对和解决的问题,在用户体验至上的今天,解决好应用的性能问题能带来非常大的收益。
Java作为最流行的编程语言之一,其应用性能诊断一直受到业界广泛关注。可能造成Java应用出现性能问题的因素非常多,例如线程控制、磁盘读写、数据库访问、网络I/0、垃圾收集等。想要定位这些问题,一款优秀的性能诊断工具必不可少。
体会1:使用数据说明问题,使用知识分析问题,使用工具处理问题。
体会2:无监控、不调优!
2.1.1 简单命令行工具
在我们刚接触java学习的时候,大家肯定最先了解的两个命令就是javac,java,那么除此之外,还有没有其他的命令可以供我们使用呢?我们进入到安装jdk的bin目录,发现还有一系列辅助工具。这些辅助工具用来获取目标JVM 不同方面、不同层次的信息,帮助开发人员很好地解决Java应用程序的一些疑难杂症。
2.2 jps:查看正在运行的Java进程
2.2.1 基本情况
jps( Java Process status):
显示指定系统内所有的HotSpot虚拟机进程(查看虚拟机进程信息),可用于查询正在运行的虚拟机进程。
说明:对于本地虚拟机进程来说,进程的本地虚拟机ID与操作系统的进程ID是一致的,是唯一的。
2.2.2 测试
10024:ScannerTest是测试的程序
6828:jps自身进程,再次输入jps命令,进程id变了,说明jps在执行完后会终止进程,再次打开,重新开启进程
13260:虚拟机自身进程
2.2.3 基本语法
jps [-help]
jps [-q] [-mlvV] [
options参数
-q:仅仅显示LVMID (local virtual machine id),即本地虚拟机唯一id。不显示主类的名称等
-1:输出应用程序主类的全类名或如果进程执行的是jar包,则输出jar完整路径
-m:输出虚拟机进程启动时传递给主类main()的参数
-v:列出虚拟机进程启动时的JVM参数。比如: -Xms20m -Xm×5em是启动程序指定的jvm参数。
说明:以上参数可以综合使用。
补充:
如果某Java进程关闭了默认开启的UsePerfData参数(即使用参数
-XX:-UsePerfData),那么jps命令(以及下面介绍的jstat)将无法探知该Java进程。
2.2.3.1 jps -q
2.2.3.2 jps -l
2.2.3.3 jps -m
2.2.3.3 jps -v
2.2.3.4 补充演示
在idea VM option 中加入参数
加入-UserPerData后,该进程不显示
改为+UserPerData后显示,默认显示
2.3 jstat:查看JVM的统计信息
2.3.1 基本情况
jstat(JVM Statistics Monitpring Tool):用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、了IT编译等运行数据。
在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。常用于检测垃圾回收问题以及内存泄漏问题。
官方文档:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html
2.3.2 基本语法
它的基本使用语法为:
jstat -
2.3.2.1 option参数
选项option可以由以下值构成。
- 类装载相关的:
- -class:显示ClassLoader的相关信息:类的装载、卸载数量、总空间、类装载所消耗的时间等
- 垃圾回收相关的:
- -gc: 显示与GC相关的堆信息。包括Eden区、两个Survivor区、老年代、永久代等的容量、已用空间、GC时间合计等信息。
- -gccapacity: 显示内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间。
- -gcutil: 显示内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比。
- -gccause: 与-gcutil功能一样,但是会额外输出导致最后一次或当前正在发生的GC产生的原因。
- -gcnew: 显示新生代Gc状况
- -gcnewcapacity: 显示内容与-gcnew基本相同,输出主要关注使用到的最大、最小空间
- -geold: 显示老年代Gc状况
2.3.2.1.1 -gc
2.3.2.1.2 -gcutil
2.3.2.1.3 -gccause
- JIT相关的:
-
-compiler:显示JIT编译器编译过的方法、耗时等信息
-
-printcompilation:输出已经被JIT编译的方法
-
2.3.2.2 interval参数
用于指定输出统计数据的周期,单位为毫秒。即:查询间隔
直到程序终止或者内存溢出,会结束打印,也可以Ctrl+c终止打印
2.3.2.3 count参数
用于指定查询的总次数
2.3.2.4 -t参数
可以在输出信息前加上一个Timestamp列,显示程序的运行时间。单位为秒
经验:我们可以比较Java进程的启动时间以及总GC时间(GCT 列),或者两次测量的间隔时间以及总GC时间的增量,来得出 GC时间占运行时间的比例。
如果该比例超过20%,则说明目前堆的压力较大;如果该比例超过90%,则说明堆里几乎没有可用空间,随时都可能抛出 OOM异常。
两次打印timestamp(程序运行时长)的差值与GCT的差值(GC的时长)
2.3.2.5 -h参数
可以在周期性数据输出时,输出多少行数据后输出一个表头信息
2.3.3 补充
jstat还可以用来判断是否出现内存泄漏。
第1步:
在长时间运行的 Java程序中,我们可以运行jstat命令连续获取多行性能数据,并取这几行数据中 OU列(即已占用的老年代内存)的最小值。
第2步:
然后,我们每隔一段较长的时间重复一次上述操作,来获得多组oU最小值。如果这些值呈上涨趋势,则说明该Java程序的老年代内存已使用量在不断上涨,这意味着无法回收的对象在不断增加,因此很有可能存在内存泄漏。
2.4 jinfo:实时查看和修改JVM配置参数
2.4.1 基本情况
基本情况
jinfo(Configuration Info for Java)
查看虚拟机配置参数信息,也可用于调整虚拟机的配置参数。
在很多情况下,Java应用程序不会指定所有的Java虚拟机参数。而此时,开发人员可能不知道某一个具体的Java虚拟机参数的默认值。在这种情况下,可能需要通过查找文档获取某个参数的默认值。这个查找过程可能是非常艰难的。但有了jinfo工具,开发人员可以很方便地找到Java虚拟机参数的当前值。
2.4.2 基本语法
它的基本使用语法为:jinfo [ options ] pid
说明:java进程ID必须要加上
2.4.2.1 jinfo -sysprops PID
可以查看由System.getProperties()取得的参数
2.4.2.2 jinfo -flags PID
查看曾经赋过值的一些参数
2.4.2.3 jinfo -flag 具体参数 PID
查看某个java进程的具体参数的值
例如:是否使用并行垃圾回收器
例如:是否使用串行的垃圾回收器
例如:是否使用G1垃圾回收器
例如:显示堆空间最大值
2.4.2.4 修改
针对Boolean类型,jinfo -flag[+|-]具体参数 PID
针对非boolean类型,jinfo -flag具体参数=具体参数值PID
jinfo不仅可以查看运行时某一个Java虚拟机参数的实际取值,甚至可以在运行时修改部分参数,并使之立即生效。
但是,并非所有参数都支持动态修改。参数只有被标记为manageable的flag可以被实时修改。其实,这个修改能力是极其有限的。
例如:PrintGCDetails
例如:MaxHeapFreeRatio
2.4.3 拓展
java -XX:+PrintFlagsInitial 查看所有JVM参数启动的初始值
java -XX:+PrintFlagsFinal 查看所有JVM参数的最终值
java -XX:+PrintCommandLineFlags 查看那些已经被用户或者JVM设置过的详细的XX参数的名称和值
2.5 jmap:导出内存映像文件&内存使用情况
2.5.1 基本情况
jmap(JVM Memory Map):作用一方面是获取dump文件(堆转储快照文件,二进制文件),它还可以获取目标Java进程的内存相关信息,包括Java堆各区域的使用情况、堆中对象的统计信息、类加载信息等。
开发人员可以在控制台中输入命令“jmap -help”查阅jmap工具的具体使用方式和一些标准选项配置。
官方帮助文档:
https://docs.oracle.com/en/java/javase/11/tools/jmap.html
2.5.2 基本语法
它的基本使用语法为:
- jmap [option]
- jmap [option] <executable
<core>
- jmap [option] [server_id@]
其中option包括
说明:这些参数和linux下输入显示的命令多少会有不同,包括也受jdk版本的影响。
2.5.2.1 -dump
- 生成Java堆转储快照:dump文件
当堆空间或者元空间等发生内存溢出时,需要当时的数据分析出是哪些数据造成溢出,而dump文件正是记录溢出时数据载体 - 特别的: -dump:live只保存堆中的存活对象
2.5.2.2 -heap
输出整个堆空间的详细信息,包括GC的使用、堆配置信息,以及内存的使用信息等
2.5.2.3 -histo
输出堆中对象的统计信息,包括类、实例数量和合计容量
特别的:-histo:live只统计堆中的存活对象
2.5.2.4 -permstat
以ClassLoader为统计口径输出永久代的内存状态信息,仅linux/solaris平台有效
2.5.2.5 -finalizerinfo
显示在F-Queue中等待Finalizer线程执行finalize方法的对象,仅linux/solaris平台有效
2.5.2.6 -F
当虚拟机进程对-dump选项没有任何响应时,可使用此选项强制执行生成dump文件,仅linux/solaris平台有效
2.5.2.7 -h|-help
jmap工具使用的帮助命令
2.5.2.8 -J<flag>
传递参数给jmap启动的jvm
2.5.2.9 使用1:导出内存映像文件
一般来说,使用jmap指令生成dump文件的操作算得上是最常用的jmap命令之一,将堆中所有存活对象导出至一个文件之中。
Heap Dump又叫做堆存储文件,指一个Java进程在某个时间点的内存快照。Heap Dump在触发内存快照的时候会保存此刻的信息如下:
- All objects
class,fields ,primitive values and references - All Classes|
ClassLoader, name , super class,static fields - Garbage collection Roots
objects defined to be reachable by the JVM - Thread Stacks and Local Variables
The call-stacks of threads at the moment of the snapshot,and per-frameinformation about local objects
说明:
1.通常在写Heap Dump文件前会触发一次Full Gc,所以heap dump文件里保存的都是FullGC后留下的对象信息。
2.由于生成dump文件比较耗时,因此大家需要耐心等待,尤其是大内存镜像生成dump文件则需要耗费更长的时间来完成。
2.5.2.9.1 手动的方式
-
jmap -dump:format=b,file= <filename.hprof> <pid>
format=b用于格式化输出,适配hprof格式
-
jmap -dump:live,format=b,file=<filename.hprof> <pid>
只查询存活的对象
场景:现场出现OOM了,运维人员把几百MB的dump文件传给后端人员分析,时间太长,可以加上:live命令,减少文件大小,提高传输效率
2.5.2.9.2 自动的方式
当程序发生OOM退出系统时,一些瞬时信息都随着程序的终止而消失,而重现OOM问题往往比较困难或者耗时。此时若能在OOM时,自动导出dump文件就显得非常迫切。
这里介绍一种比较常用的取得堆快照文件的方法,即使用:
-XX:+HeapDumpOnOutOfMemoryError:在程序发生OOM时,导出应用程序的当前堆快照。
-XX:HeapDumpPath:可以指定堆快照的保存位置。
比如:
-Xmx100m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D: \m.hprof
2.5.2.10 使用2:显示堆内存相关信息
-
jmap -heap pid
显示程序运行这个一刻的堆使用情况
-
jmap -histo pid
2.5.2.11 其它作用
jmap -permstat pid
查看系统的ClassLoader信息
jmap -finalizerinfo
查看堆积在finalizer队列中的对象
2.5.2.12 小结
由于jmap将访问堆中的所有对象,为了保证在此过程中不被应用线程干扰,jmap需要借助安全点机制,让所有线程停留在不改变堆中数据的状态。也就是说,由jmap导出的堆快照必定是安全点位置的。这可能导致基于该堆快照的分析结果存在偏差。
举个例子,假设在编译生成的机器码中,某些对象的生命周期在两个安全点之间,那么: live选项将无法探知到这些对象。
另外,如果某个线程长时间无法跑到安全点,jmap将一直等下去。与前面讲的jstat则不同,垃圾回收器会主动将jstat所需要的摘要数据保存至固定位置之中,而jstat只需直接读取即可。
2.6 jhat:JDK自带堆分析工具
2.6.1 基本情况
jhat(JVM Heap Analysis Tool):
Sun JDK提供的jhat命令与jmap命令搭配使用,用于分析jmap生成的heap dump文件(堆转储快照)。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,用户可以在浏览器中查看分析结果(分析虚拟机转储快照信息)。
使用了jhat命令,就启动了一个http服务,端口是7000,即http://localhost:7000/,就可以在浏览器里分析。
说明:jhat命令在JDK9、JDK10中已经被删除,官方建议用VisualVM代替。
打开http://localhost:7000/服务
Ctrl+C关闭jhat服务
2.6.2 基本语法
- option参数: -stack falseltrue
关闭|打开对象分配调用栈跟踪 - option参数: -refs falseltrue
关闭|打开对象引用跟踪 - option参数: -port port-number
设置jhat HTTP Server的端口号,默认7000 - option参数: -exclude exclude-file
执行对象查询时需要排除的数据成员 - option参数: -baseline exclude-file
指定一个基准堆转储 - option参数: -debug int
设置debug级别 - option参数:-version
启动后显示版本信息就退出 - option参数:
-J<flag>
传入启动参数,比如-J-Xmx512m
2.7 jstack:打印JVM中线程快照
2.7.1 基本情况
jstack(JVM Stack Trace):用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪)。线程快照就是当前虚拟机内指定进程的每一条线程正在执行的方法堆栈的集合。
生成线程快照的作用:可用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。这些都是导致线程长时间停顿的常见原因。当线程出现停顿时,就可以用jstack显示各个线程调用的堆栈情况。
官方帮助文档:
https: //docs.oracle.com/en/java/javase/11/tools/jstack.html
在thread dump中,要留意下面几种状态
- 死锁,Deadlock(重点关注)
- 等待资源,waiting on condition(重点关注)
- 等待获取监视器,Waiting on monitor entry(重点关注)·阻塞,Blocked(重点关注)
- 执行中,Runnable
- 暂停,Suspended
2.7.2 基本语法
它的基本使用语法为:jstack option pid
jstack管理远程进程的话,需要在远程程序的启动参数中增加:
-Djava.rmi.server.hostname=…..
-Dcom.sun.management.jmxremote
-Dcom.sun.management .jmxremote. port=8888
示例:模拟线程死锁
代码:
`public class ThreadDeadLock {
public static void main(String[] args) {
StringBuilder s1 = new StringBuilder();
StringBuilder s2 = new StringBuilder();
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}`