性能诊断是软件工程师在日常生活中需要经常面对的问题,在用户体验至上的今天,解决好应用的性能问题能带来非常大的收益。
Java作为最流行的编程语言之一,其应用的性能诊断能力一直受到业界的广泛关注。可能造成Java应用出现性能问题的因素非常多,例如线程控制,磁盘读写,数据库访问,网络I/O,垃圾收集等。想要定位这些问题,优秀的性能诊断工具必不可少。
package priv.user.jvm;
import java.util.Scanner;
/**
* Description: 利用扫描器的阻塞状态验证jps的使用
* Author:江洋大盗
* Date:2021/3/11 21:08
*/
public class JPSTest {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
scanner.next();
}
}
jps [ options ] [ hostid ]
options参数:
补充:如果Java某进程关闭了默认开启的UsePerfData参数(即使用参数:-Xx:-UsePerfData),那么jps指令以及下面介绍的jstat指令将无法探知该Java进程。
jstat(JVM Statistics Monitoring Tool),用于监视虚拟机各种运行状态信息的命令行工具,它可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集等信息。
在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具,常用于检测垃圾回收问题以及内存泄漏问题。
jstat -< option > [ -t ] [ -h < lines > ] < vmid > [ < interval > [ < count > ] ]
查看命令相关参数:
jstat -h 或 jstat -help
用于指定输出统计数据的周期,单位为毫秒,即查询间隔,不指定interval时默认打印一次
用于指定查询的总次数
可以在输出信息上加一个Timestamp列,显示程序的运行时间,单位秒
选项option参数可以由以下值构成:
类装载相关的:
垃圾回收相关的:
JIT相关的
查看虚拟机配置参数信息,也可用于调整虚拟机的配置参数
在很多情况下,Java应用程序不会指定所有的Java虚拟机参数,而此时,开发人员可能不知道某一个具体的Java虚拟机参数的默认值。在这种情况下,可能需要查找文档获取某个参数的默认值,这个查找过程可能是非常艰难的。但有了jinfo工具,开发人员可以很方便的找到Java虚拟机参数的当前值。
jinfo [ options ] pid
说明 ,java进程id必须要加上
【options参数】
选项 | 参数说明 |
---|---|
no options | 输出全部的参数和系统属性 |
-flag name | 输出对应名称的参数 |
-flag [±]name | 开启或者关闭对应名称的参数,只有被标记为manageable的参数才可以被动态修改 |
-flag name=value | 设定对应名称的参数 |
-flags | 输出全部参数 |
-sysprops | 输出系统属性 |
可以查看被标记为manageable的参数
Linux系统下的指令
java -XX:+PrintFlagsFinal -version | grep manageable
jmap(JVM Memory Map):作用一方面是获取dump文件(堆转储快照文件,二进制文件),它还可以获取目标Java进程的内存相关信息,包括Java堆各区域的使用情况、堆中对象的统计信息、类加载信息等。
开发人员可以在控制台中输入命令“jmap -help”查阅jmap工具的具体使用方式和一些标准选项配置。
官方帮助文档点这里
它的基本使用语法为:
其中option包括:
选项 | 作用 |
---|---|
-dump | 生成Java堆转储快照:dump文件,特别的: -dump:live只保存堆中的存活对象 |
-heap | 输出整个堆空间的详细信息,包括GC的使用、堆配置信息,以及内存的使用信息等 |
-histo | 输出堆空间中对象的统计信息,包括类、实例数量和合计容量 ,特别的: -histo:live只统计堆中的存活对象 |
-permstat | 以ClassLoader为统计口径输出永久代的内存状态信息,仅linux/solaris平台有效 |
-finalizerinfo | 显示在F-Queue中等待Finalizer线程执行finalize方法的对象,仅linux/solaris平台有效 |
-F | 当虚拟机进程对-dump选项没有任何响应时,强制执行生成dump文件,仅linux/solaris平台有效 |
-h / -help | jmap工具使用的帮助命令 |
-J< flag > | 传递参数给jmap启动的jum |
说明:这些参数和linux下输入显示的命令多少会有不同,包括也受jdk版本的影响。
一般来说,使用jmap指令生成dump文件的操作算得上是最常用的jmap命令之一,将堆中所有存活对象导出至一个文件之中。
Heap Dump又叫做堆存储文件,指一个Java进程在某个时间点的内存快照。Heap Dump在触发内存快照的时候会保存此刻的信息如下:
说明:
测试代码:
package priv.user.jvm;
import java.util.ArrayList;
/**
* Description:
* Author:江洋大盗
* Date:2021/3/15 21:53
* -Xms60m -Xmx60m -XX:SurvivorRatio=8
*/
public class JMAPTest {
public static void main(String[] args) {
ArrayList<byte[]> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
byte[] bytes = new byte[1024*100];//100kb
list.add(bytes);
try {
Thread.sleep(120);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
当程序发生OOM退出系统时,一些瞬时信息都随着程序的终止而消失,而重现oOM问题往往比较困难或者耗时。此时若能在OOM时,自动导出dump文件就显得非常迫切。
这里介绍一种比较常用的取得堆快照文件的方法,即使用:
-XX:+HeapDumpOnOutOfMemoryError:在程序发生OOM时,导出应用程序的当前堆快照。
-XX:HeapDumpPath:可以指定堆快照的保存位置。
比如:
-Xmx10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\m.hprof
查看堆中当前时刻的内存使用情况
分析堆中的对象内容
查看系统的ClassLoader信息
查看堆积在finalizer队列中的对象
由于jmap将访问堆中的所有对象,为了保证在此过程中不被应用线程干扰,jmap需要借助安全点机制,让所有线程停留在不改变堆中数据的状态。也就是说,由jmap导出的堆快照必定是安全点位置的。这可能导致基于该堆快照的分析结果存在偏差。
举个例子,假设在编译生成的机器码中,某些对象的生命周期在两个安全点之间,那么: live选项将无法探知到这些对象。
另外,如果某个线程长时间无法跑到安全点,jmap将一直等下去。
与前面讲的jstat则不同,垃圾回收器会主动将jstat所需要的摘要数据保存至固定位置之中,而jstat只需直接读取即可。
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代替。
它的基本使用语法为:
jhat [option] [ dumpfile ]
option参数:
参数 | 作用 |
---|---|
option参数:-stack false/true | 关闭 |
option参数: -refs false/true | 关闭 |
option参数: -port port-number | Server的端口号,默认7000 |
option参数: -exclude exclude-file | 执行对象查询时需要排除的数据成员 |
option参数: -baseline exclude-file | 指定一个基准堆转储 |
option参数: -debug int | 设置debug级别 |
option参数: -version | 启动后显示版本信息就退出 |
option参数: -J< flag > | 传入启动参数,比如-J-Xmx512m |
jstack(JVM Stack Trace):用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪)。线程快照就是当前虚拟机内指定进程的每一条线程正在执行的方法堆栈的集合。
生成线程快照的作用:可用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。这些都是导致线程长时间停顿的常见原因。当线程出现停顿时,就可以用jstack显示各个线程调用的堆栈情况。
官方帮助文档:
在thread dump中,要留意下面几种状态:
死锁测试代码:
package priv.user.jvm;
/**
* Description:
* Author:江洋大盗
* Date:2021/3/16 10:02
*/
public class ThreadDeadLock {
public static void main(String[] args) {
StringBuilder s1 = new StringBuilder();
StringBuilder s2 = new StringBuilder();
new Thread(()->{
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(()->{
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();
}
}
package priv.user.jvm;
/**
* Description:
* Author:江洋大盗
* Date:2021/3/16 10:20
*/
public class ThreadSleep {
public static void main(String[] args) {
System.out.println("hello world");
try {
Thread.sleep(1000 * 60 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package priv.user.jvm;
/**
* Description:
* Author:江洋大盗
* Date:2021/3/16 10:24
*/
public class ThreadSynTest {
public static void main(String[] args) {
GetNum num = new GetNum();
Thread t1 = new Thread(num);
Thread t2 = new Thread(num);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
class GetNum implements Runnable {
private int num = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
if (num <= 100) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
} else {
break;
}
}
}
}
}
option参数
参数 | 作用 |
---|---|
-F | 当正常输出的请求不被响应时,强制输出线程堆栈 |
-l | 除堆栈外,显示关于锁的附加信息 |
-m | 如果调用到本地方法的话,可以显示C/C++的堆栈 |
-h | 帮助操作 |
补充:
Java的Thread类中有一个方法可以输出当前的线程信息,不过效果不如jstack明显:
package priv.user.jvm;
import java.util.Map;
/**
* Description:
* Author:江洋大盗
* Date:2021/3/16 10:46
*/
public class AllThreadTrace {
public static void main(String[] args) {
Map<Thread, StackTraceElement[]> all = Thread.getAllStackTraces();
for (Map.Entry<Thread, StackTraceElement[]> entry : all.entrySet()) {
Thread t = entry.getKey();
StackTraceElement[] v = entry.getValue();
System.out.println("【Thread name:" + t.getName() + "】");
for (StackTraceElement s : v) {
System.out.println("\t" + s.toString());
}
}
}
}
在JDK 1.7以后,新增了一个命令行工具jcmd。
它是一个多功能的工具,可以用来实现前面除了jstat之外所有命令的功能。比如:用它来导出堆、内存使用、查看Java进程、导出线程信息、执行GC、JVM运行时间等。
官方帮助文档
jcmd拥有jmap的大部分功能,并且在Oracle的官方网站上也推荐使用jcmd命令代jmap命令
之前的指令只涉及到监控本机的Java应用程序,而在这些工具中,一些监控工具也支持对远程计算机的监控(如jps、jstat)。为了启用远程监控,则需要配合使用jstatd 工具。
命令jstatd是一个RMI服务端程序,它的作用相当于代理服务器,建立本地计算机与远程监控工具的通信。jstatd服务器将本机的Java应用程序信息传递到远程计算机。
除了奋斗,我们别无选择