【深入理解java虚拟机】7. 虚拟机性能监控、故障处理工具
- 1. jps : 虚拟机进程状况工具
- 2. jstat:虚拟机统计信息监视工具
- 3. jinfo:Java配置信息工具
- 4. jmap:Java内存映像工具
- 5. jhat:虚拟机堆转储快照分析工具
- 6. jstack:Java堆栈跟踪工具
- 7. 性能监控和故障处理工具汇总
- 8. 可视化故障处理工具
1. jps : 虚拟机进程状况工具
可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(LVMID,Local Virtual Machine Identifier)。
LVMID与操作系统的进程ID(PID,Process Identifier)是一致的。
~ % jps -l
12696 sun.tools.jps.Jps
5305 com.lb.jvm.gc.ReferenceCountingGC
jps工具主要选项
2. jstat:虚拟机统计信息监视工具
jstat(JVM Statistics Monitoring Tool), 用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据,在没有GUI图形界面、只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的常用工具。
jstat命令格式为:
jstat [ option vmid [interval[s|ms] [count]] ]
如果是远程虚拟机进程,那VMID的格式应当是:
[protocol:][//]lvmid[@hostname[:port]/servername]
参数interval和count代表查询间隔和次数,如果省略这2个参数,说明只查询一次。假设需要每250毫秒查询一次进程2764垃圾收集状况,一共查询20次,那命令应当是:
jstat -gc 2764 250 20
选项option代表用户希望查询的虚拟机信息,主要分为三类:类加载、垃圾收集、运行期编译状况。
jstat工具主要选项jstat执行样例(jdk 8 之前)
jstat -gcutil 2764
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 0.00 6.20 41.42 47.20 16 0.105 3 0.472 0.577
E,表示Eden使用了6.2%的空间
S0、S1,(表示Survivor0、Survivor1)里面都是空的
O,表示Old使用了41.42%的空间
P,表示Permanent,永久代使用了47.20%
YGC,表示Young GC,发生16次,总耗时0.105秒
FGC,表示Full GC 发生3次,总耗时(FGCT,表示Full GC Time)为0.472秒
GCT, 所有GC总耗时(GCT,表示GC Time)为0.577秒。
jstat执行样例(jdk 8 )
imageS0C:年轻代中第一个幸存区的大小
S1C:年轻代中第二个幸存区的大小
S0U:年轻代中第一个幸存区的使用大小
S1U:年轻代中第二个幸存区的使用大小
EC:年轻代中伊甸园区的大小
EU:年轻代中伊甸园区的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法区大小
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
YGC:年轻代gc次数
YGCT:年轻代消耗时间
FGC:老年代gc次数
FGCT:老年代gc消耗时间
GCT:gc消耗总时间
3. jinfo:Java配置信息工具
jinfo(Configuration Info for Java)的作用是实时查看和调整虚拟机各项参数。使用jps命令的-v参数可以查看虚拟机启动时显式指定的参数列表,但如果想知道未被显式指定的参数的系统默认值,除了去找资料外,就只能使用jinfo的-flag选项进行查询了(如果只限于JDK 6或以上版本的话,使用java-XX:+PrintFlagsFinal查看参数默认值也是一个很好的选择)。
jinfo还可以使用-sysprops选项把虚拟机进程的System.getProperties()的内容打印出来。这个命令在JDK 5时期已经随着Linux版的JDK发布,当时只提供了信息查询的功能,JDK 6之后,jinfo在Windows和Linux平台都有提供,并且加入了在运行期修改部分参数值的能力(可以使用-flag[+|-]name或者-flag name=value在运行期修改一部分运行期可写的虚拟机参数值)。
jinfo命令格式:
jinfo [ option ] pid
执行样例:查询CMSInitiatingOccupancyFraction参数值
jinfo -flag CMSInitiatingOccupancyFraction 1444 -XX:CMSInitiatingOccupancyFraction=85
4. jmap:Java内存映像工具
jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为heapdump或dump文件)。
如果不使用jmap命令,要想获取Java堆转储快照也还有一些比较“暴力”的手段:
- -XX:+HeapDumpOnOutOfMemoryError参数,可以让虚拟机在内存溢出异常出现之后自动生成堆转储快照文件,
- -XX:+HeapDumpOnCtrlBreak参数则可以使用[Ctrl]+[Break]键让虚拟机生成堆转储快照文件,又或者在Linux系统下通过Kill-3命令发送进程退出信号“恐吓”一下虚拟机,也能顺利拿到堆转储快照。
jmap的作用并不仅仅是为了获取堆转储快照,它还可以查询finalize执行队列、Java堆和方法区的详细信息,如空间使用率、当前用的是哪种收集器等。
jmap命令格式:
jmap [ option ] vmid
jmap工具主要选项
5. jhat:虚拟机堆转储快照分析工具
JDK提供jhat(JVM Heap Analysis Tool)命令与jmap搭配使用,来分析jmap生成的堆转储快照。
jhat内置了一个微型的HTTP/Web服务器,生成堆转储快照的分析结果后,可以在浏览器中查看。
jhat dump.dump
image
6. jstack:Java堆栈跟踪工具
jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。
线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。
线程出现停顿时通过jstack来查看各个线程的调用堆栈,就可以获知没有响应的线程到底在后台做些什么事情,或者等待着什么资源。
jstack命令格式:
jstack [ option ] vmid
image
7. 性能监控和故障处理工具汇总
image8. 可视化故障处理工具
JDK中除了附带大量的命令行工具外,还提供了几个功能集成度更高的可视化工具,用户可以使用这些可视化工具以更加便捷的方式进行进程故障诊断和调试工作。这类工具主要包括JConsole、JHSDB、VisualVM和JMC四个。
-
jConsole
JConsole是最古老,早在JDK 5时期就已经存在的虚拟机监控工具
-
JHSDB
虽然名义上是JDK 9中才正式提供,但之前已经以sa-jdi.jar包里面的HSDB(可视化工具)和CLHSDB(命令行工具)的形式存在了很长一段时间。
-
VisualVM
VisualVM在JDK 6 Update 7中首次发布,直到JRockit Mission Control与OracleJDK的融合工作完成之前,它都曾是Oracle主力推动的多合一故障处理工具,现在它已经从OracleJDK中分离出来,成为一个独立发展的开源项目。
4.Java Mission Control
1. JHSDB
JDK中提供了JCMD和JHSDB两个集成式的多功能工具箱,它们不仅整合了上一节介绍到的所有基础工具所能提供的专项功能,而且由于有着“后发优势”,能够做得往往比之前的老工具们更好、更强大。
imageJHSDB是一款基于服务性代理(Serviceability Agent,SA)实现的进程外调试工具。服务性代理是HotSpot虚拟机中一组用于映射Java虚拟机运行信息的、主要基于Java语言(含少量JNI代码)实现的API集合。服务性代理以HotSpot内部的数据结构为参照物进行设计,把这些C++的数据抽象出Java模型对象,相当于HotSpot的C++代码的一个镜像。通过服务性代理的API,可以在一个独立的Java虚拟机的进程里分析其他HotSpot虚拟机的内部数据,或者从HotSpot虚拟机进程内存中dump出来的转储快照里还原出它的运行状态细节。
测试staticObj、instanceObj、localObj存放在哪里
/**
* staticObj、instanceObj、localObj存放在哪里?
*/
public class JHSDB_TestCase {
static class Test {
static JHSDB_TestCase.ObjectHolder staticObj = new JHSDB_TestCase.ObjectHolder();
JHSDB_TestCase.ObjectHolder instanceObj = new JHSDB_TestCase.ObjectHolder();
void foo() {
JHSDB_TestCase.ObjectHolder localObj = new JHSDB_TestCase.ObjectHolder();
System.out.println("done"); // 这里设一个断点
}
}
private static class ObjectHolder {
}
public static void main(String[] args) {
Test test = new JHSDB_TestCase.Test();
test.foo();
}
}
由于JHSDB本身对压缩指针的支持存在很多缺陷,建议用64位系统的读者在实验时禁用压缩指针,另外为了后续操作时可以加快在内存中搜索对象的速度,也建议读者限制一下Java堆的大小。
-Xmx10m -XX:+UseSerialGC -XX:-UseCompressedOops
jps 查询java进程id
xxx@% jps
12705
17986 Jps
5305 ReferenceCountingGC
17978 Launcher
17979 JHSDB_TestCase
17758 Launcher
17759 SpringbootSimpleStarterApplication
17743 App
使用以下命令进入JHSDB的图形化模式,并使其附加进程17979:
// jdk9
jhsdb hsdb --pid 11180
// jdk8 跳转到java_home的lib下
java -cp ./sa-jdi.jar sun.jvm.hotspot.HSDB
// linxu 或者 mac一定要加sudo , 不然唤醒不起来会找不到主类
image
查看堆内存的虚拟内存地址起止范围
image打开Windows console,搜索Eden初始地址到to 截止地址的 对象
scanoops 0x0000000114800000 0x0000000114b50000
可以看到三个ObjectHolder的内存地址
imagerevptrs 内存地址 ,查看对象的引用
只有成员变量可以看到,静态成员变量都不行(不支持查找栈上引用)
revptrs 0x0000000114a03888
image
查看main方法的栈内存
image image2. JConsole:Java监视与管理控制台
JConsole(Java Monitoring and Management Console)是一款基于JMX(Java Manage-ment Extensions)的可视化监视、管理工具。它的主要功能是通过JMX的MBean(Managed Bean)对系统进行信息收集和参数动态调整。
2.1. 启动JConsole
windows
通过JDK/bin目录下的jconsole.exe启动JCon-sole后,会自动搜索出本机运行的所有虚拟机进程,而不需要用户自己使用jps来查询。
Mac || linux
直接在命令行输入jconsole
image image“概述”页签里显示的是整个虚拟机主要运行数据的概览信息,包括“堆内存使用情况”“线程”“类”“CPU使用情况”四项信息的曲线图,这些曲线图是后面“内存”“线程”“类”页签的信息汇总。
2.2. 内存监控
“内存”页签的作用相当于可视化的jstat命令,用于监视被收集器管理的虚拟机内存(被收集器直接管理的Java堆和被间接管理的方法区)的变化趋势。
测试代码
虚拟器参数
-Xms100m -Xmx100m -XX:+UseSerialGC
public class A {
/**
* 内存占位符对象,一个OOMObject大约占64KB
*/
static class OOMObject {
public byte[] placeholder = new byte[64 * 1024];
}
public static void fillHeap(int num) throws InterruptedException {
List<OOMObject> list = new ArrayList<OOMObject>();
for (int i = 0; i < num; i++) { // 稍作延时,令监视曲线的变化更加明显
Thread.sleep(50);
list.add(new OOMObject());
}
System.gc();
}
public static void main(String[] args) throws Exception {
fillHeap(1000);
}
}
这段代码的作用是以64KB/50ms的速度向Java堆中填充数据,一共填充1000次,使用JConsole的“内存”页签进行监视,观察曲线和柱状指示图的变化。
程序运行后,在“内存”页签中可以看到内存池Eden区的运行趋势呈现折线状,监视范围扩大至整个堆后,会发现曲线是一直平滑向上增长的。
image2.3 线程监控
如果说JConsole的“内存”页签相当于可视化的jstat命令的话,那“线程”页签的功能就相当于可视化的jstack命令了,遇到线程停顿的时候可以使用这个页签的功能进行分析。
前面讲解jstack命令时提到线程长时间停顿的主要原因有等待外部资源(数据库连接、网络资源、设备资源等)、死循环、锁等待等,
public class A {
public static void createBusyThread() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true){}
// 第41行 ;
}
}, "testBusyThread"); thread.start();
}
/**
* 线程锁等待演示
*/
public static void createLockThread(final Object lock) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "testLockThread");
thread.start();
}
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
br.readLine();
createBusyThread();
br.readLine();
Object obj = new Object();
createLockThread(obj);
}
}
程序运行后,首先在“线程”页签中选择main线程,如图4-13所示。堆栈追踪显示BufferedReader的readBytes()方法正在等待System.in的键盘输入,这时候线程为Runnable状态,Runnable状态的线程仍会被分配运行时间,但readBytes()方法检查到流没有更新就会立刻归还执行令牌给操作系统,这种等待只消耗很小的处理器资源。
imageRunnable状态
接着监控testBusyThread线程,。testBusyThread线程一直在执行空循环,从堆栈追踪中,41行的代码为while(true)。这时候线程为Runnable状态,而且没有归还线程执行令牌的动作,所以会在空循环耗尽操作系统分配给它的执行时间,直到线程切换为止,这种等待会消耗大量的处理器资源。
imageWaitting状态
显示testLockThread线程在等待lock对象的notify()或notifyAll()方法的出现,线程这时候处于WAITING状态,在重新唤醒前不会被分配执行时间。
testLockThread线程testLockThread线程正处于正常的活锁等待中,只要lock对象的notify()或notifyAll()方法被调用,这个线程便能激活继续执行。代码清单4-9演示了一个无法再被激活的死锁等待。
Blocked死锁状态
代码清单4-9 死锁代码样例
public class Deadlock {
public static String str1 = "str1";
public static String str2 = "str2";
public static void main(String[] args) {
Thread a = new Thread(() -> {
try {
while (true) {
synchronized (Deadlock.str1) {
System.out.println(Thread.currentThread().getName() + "锁住 str1");
Thread.sleep(1000);
synchronized (Deadlock.str2) {
System.out.println(Thread.currentThread().getName() + "锁住 str2");
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
});
Thread b = new Thread(() -> {
try {
while (true) {
synchronized (Deadlock.str2) {
System.out.println(Thread.currentThread().getName() + "锁住 str2");
Thread.sleep(1000);
synchronized (Deadlock.str1) {
System.out.println(Thread.currentThread().getName() + "锁住 str1");
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
});
a.start();
b.start();
}
}
发现线程t1,t2都在拿第二把锁的地方被锁住了
image image
可以点检测死锁,查看所有处于死锁状态的线程
image3. VisualVM:多合-故障处理工具
VisualVM(All-in-One Java Troubleshooting Tool)是功能最强大的运行监视和故障处理程序之一,曾经在很长一段时间内是Oracle官方主力发展的虚拟机故障处理工具。
3.1. VisualVM兼容范围与插件安装
VisualVM基于NetBeans平台开发工具,所以一开始它就具备了通过插件扩展功能的能力,有了插件扩展支持,VisualVM可以做到:
-
显示虚拟机进程以及进程的配置、环境信息(jps、jinfo)。
-
监视应用程序的处理器、垃圾收集、堆、方法区以及线程的信息(jstat、jstack)。
-
dump以及分析堆转储快照(jmap、jhat)。
-
方法级的程序运行性能分析,找出被调用最多、运行时间最长的方法。
-
离线程序快照:收集程序的运行时配置、线程dump、内存dump等信息建立一个快照,可以将快照发送开发者处进行Bug反馈。
-
其他插件带来的无限可能性。
3.1.1. 插件安装
插件安装界面 主界面3.2. 生成、浏览堆转储快照
在VisualVM中生成堆转储快照文件有两种方式,可以执行下列任一操作:
-
在“应用程序”窗口中右键单击应用程序节点,然后选择“堆Dump”。
-
在“应用程序”窗口中双击应用程序节点以打开应用程序标签,然后在“监视”标签中单击“堆Dump”。
堆页签中的“摘要”面板可以看到应用程序dump时的运行时参数、System.getPro-perties()的内容、线程堆栈等信息;“类”面板则是以类为统计口径统计类的实例数量、容量信息;“实例”面板不能直接使用,因为VisualVM在此时还无法确定用户想查看哪个类的实例,所以需要通过“类”面板进入,在“类”中选择一个需要查看的类,然后双击即可在“实例”里面看到此类的其中500个实例的具体属性信息;“OQL控制台”面板则是运行OQL查询语句的,同jhat中介绍的OQL功能一样。
3.3. 分析程序性能
在Profiler页签中,VisualVM提供了程序运行期间方法级的处理器执行时间分析以及内存分析。做Profiling分析肯定会对程序运行性能有比较大的影响,所以一般不在生产环境使用这项功能,或者改用JMC来完成,JMC的Profiling能力更强,对应用的影响非常轻微。
要开始性能分析,profile->选择cpu或者内存,然后切换到应用程序中对程序进行操作,VisualVM会记录这段时间中应用程序执行过的所有方法。
-
Cpu分析,将会统计每个方法的执行次数、执行耗时;
-
内存分析,则会统计每个方法关联的对象数以及这些对象所占的空间。等要分析的操作执行结束后,点击“停止”按钮结束监控过程。
4. BTrace动态日志跟踪
BTrace是一个很神奇的VisualVM插件,它本身也是一个可运行的独立程序。
BTrace的作用是在不中断目标程序运行的前提下,通过HotSpot虚拟机的Instrument功能动态加入原本并不存在的调试代码。
这项功能对实际生产中的程序很有意义:如当程序出现问题时,排查错误的一些必要信息时(譬如方法参数、返回值等),在开发时并没有打印到日志之中以至于不得不停掉服务时,都可以通过调试增量来加入日志代码以解决问题。
img测试代码
public class BTraceTest {
public int add(int a, int b) {
return a + b;
}
public static void main(String[] args) throws IOException {
BTraceTest test = new BTraceTest();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
for (int i = 0; i < 10; i++) {
reader.readLine();
int a = (int) Math.round(Math.random() * 1000);
int b = (int) Math.round(Math.random() * 1000);
System.out.println(test.add(a, b));
}
}
}
假设这段程序已经上线运行,而我们现在又有了新的需求,想要知道程序中生成的两个随机数是什么,但程序并没有在执行过程中输出这一点。此时,在VisualVM中打开该程序的监视,在BTrace页签填充TracingScript的内容,输入调试代码。
@BTrace
public class TracingScript {
@OnMethod(clazz = "org.fenixsoft.monitoring.BTraceTest", method = "add", location = @Location(Kind.RETURN))
public static void func(@Self org.fenixsoft.monitoring.BTraceTest instance, int a, int b, @Return int result) {
println("调用堆栈:");
jstack();
println(strcat("方法参数A:", str(a)));
println(strcat("方法参数B:", str(b)));
println(strcat("方法结果:", str(result)));
}
}
img
5. Java Mission Control:可持续在线的监控工具
img在左侧的“JVM浏览器”面板中自动显示了通过JDP协议(Java Discovery Protocol)找到的本机正在运行的HotSpot虚拟机进程,如果需要监控其他服务器上的虚拟机,可在“文件->连接”菜单中创建远程连接。
img被监控虚拟机进程启动的时候以虚拟机参数的形式指定
-Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Djava.rmi.server.hostname=192.168.31.4 -XX:+UnlockCommercialFeatures -XX:+FlightRecorder
本地虚拟机与远程虚拟机进程的差别只限于创建连接这个步骤,连接成功创建以后的操作就是完全一样的了。把“JVM浏览器”面板中的进程展开后,可以看到每个进程的数据都有MBean和JFR两个数据来源。
5.1 MBean
关于MBean这部分数据,与JConsole和VisualVM上取到的内容是一样的,
5.2 飞行记录
双击“飞行记录器”,将会出现“启动飞行记录”窗口
img在启动飞行记录时,可以进行记录时间、垃圾收集器、编译器、方法采样、线程记录、异常记录、网络和文件I/O、事件记录等选项和频率设定,点击“完成”按钮后马上就会开始记录,记录时间结束以后会生成飞行记录报告。
飞行记录报告里包含以下几类信息:
-
一般信息:关于虚拟机、操作系统和记录的一般信息。
-
内存:关于内存管理和垃圾收集的信息。
-
代码:关于方法、异常错误、编译和类加载的信息。
-
线程:关于应用程序中线程和锁的信息。
-
I/O:关于文件和套接字输入、输出的信息。
-
系统:关于正在运行Java虚拟机的系统、进程和环境变量的信息。
-
事件:关于记录中的事件类型的信息,可以根据线程或堆栈跟踪,按照日志或图形的格式查看。
即使不考虑对被测试程序性能影响方面的优势,JFR提供的数据质量通常也要比其他工具通过代理形式采样获得或者从MBean中取得的数据高得多。以垃圾搜集为例,HotSpot的MBean中一般有各个分代大小、收集次数、时间、占用率等数据(根据收集器不同有所差别),这些都属于“结果”类的信息,而JFR中还可以看到内存中这段时间分配了哪些对象、哪些在TLAB中(或外部)分配、分配速率和压力大小如何、分配归属的线程、收集时对象分代晋升的情况等,这些就是属于“过程”类的信息,对排查问题的价值是难以估量的。