虚拟机性能监控、故障处理工具
给一个系统定位问题的时候,知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段。这里说的数据包括但不限于异常堆栈、虚拟机运行日志、垃圾收集器日志、线程快照(threaddump/javacore文件)、堆转储快照(heapdump/hprof文件)等。恰当地使用虚拟机故障处理、分析的工具可以提升我们分析数据、定位并解决问题的效率
基础故障处理工具
JDK的bin目录下提供了许多小工具
jps:虚拟机进程状况工具
可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(LVMID,Local Virtual Machine Identifier)
jps [选项] [hostid]
jps主要选项:
选项 | 作用 |
---|---|
-q | 只输出LVMID,省略主类的名称 |
-m | 输出虚拟机进程启动时传递给主类main()函数的参数 |
-l | 输出主类的全名,若进程执行的是JAR包,输出JAR路径 |
-v | 输出虚拟机进程启动时的JVM参数 |
jstat:虚拟机统计信息监视工具
jstat(JVM Statistics Monitoring Tool)是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时等数据
jstat [选项 vmid(虚拟机进程ID)] [间隔时间] [执行次数]
jinfo:Java配置信息工具
jinfo(Configuration Info for Java)的作用是实时查看和调整虚拟机各项参数
jinfo [选项] pid(操作系统进程ID)
jmap:Java内存映像工具
jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为heapdump或dump文件)
jmap [选项] vmid
jhat:虚拟机堆转储快照分析工具
JDK提供jhat(JVM Heap Analysis Tool)命令与jmap搭配使用,来分析jmap生成的堆转储快照
分析功能比较简陋,少用
jstack:Java堆栈跟踪工具
jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。线程出现停顿时通过jstack来查看各个线程的调用堆栈,就可以获知没有响应的线程到底在后台做些什么事情,或者等待着什么资源
jmap [选项] vmid
可视化故障处理工具
JHSDB:基于服务性代理的调试工具
JHSDB是一款基于服务性代理(Serviceability Agent,SA)实现的进程外调试工具。服务性代理是HotSpot虚拟机中一组用于映射Java虚拟机运行信息的、主要基于Java语言(含少量JNI代码)实现的API集合
在JDK8的bin目录下我并没有找到这个工具,为此特地下载了个JDK17进行演示
package org.example;
/**
* 使用Debug 启动,利用JHSDB工具获取staticObj,instanceObj,localObj三个变量的存储位置
* VM参数:-Xmx10m -XX:+UseSerialGC -XX:-UseCompressedOops
*/
public class Hello {
static class Test {
static ObjectHolder staticObj = new ObjectHolder();
ObjectHolder instanceObj = new ObjectHolder();
void foo() {
ObjectHolder localObj = new ObjectHolder();
System.out.println("done"); // 这里设一个断点
}
}
private static class ObjectHolder {}
public static void main(String[] args) {
Test test = new Hello.Test();
test.foo();
}
}
Debug启动上述代码,确保内存已分配好,打开命令行,去到JDK的bin目录下,使用命令
jps -l
查看虚拟机进程执行命令
.\jhsdb.exe hsdb
,打开jhsdb界面,菜单栏选择File->Attach To HotSpot Process,输入上面拿到的pid菜单栏选择Tools->Heap Parameters查看堆中信息,由于VM参数指定使用的是SerialGC(不指定默认为G1收集器),所以可以看到典型的分代布局:Eden,From,To,Old;以及它们各自的虚拟内存地址起止范围
菜单栏选择Windows->Console,使用
scanoops
命令在Java堆的新生代(从Eden起始地址到To Survivor结束地址)范围内查找ObjectHolder的实例,可以看到三个实例的地址都在Eden区地址范围内,也印证了新对象在Eden区创建的规则菜单栏选择Tools->Inspector,输入上述任一实例的地址,可查看对象信息(对象头和指向对象元数据的指针,里面包括了Java类型的名字、继承关系、实现接口关系,字段信息、方法信息、运行时常量池的指针、内嵌的虚方法表(vtable)以及接口方法表(itable)等)
根据堆中对象实例地址找出引用它们的指针,继续回到上述的Console界面,执行命令
revptrs 对象实例地址
,得出引用指针,可以看到这是一个Class类型对象实例,里面有一个staticObj
实例字段,注意一下它后面的地址,可以发现,就是这里引用了第一个对象JDK 7及其以后版本的HotSpot虚拟机选择把静态变量与类型在Java语言一端的映射Class对象存放在一起,存储于Java堆之中
同理第二,三个对象实例查找也是如此操作。可以看到第二次找到的是
Hello$Test
的对象实例,第二个ObjectHolder
的指针是在Java堆中Hello$Test
对象的instanceObj
字段上。接着查找第三个对象时发现返回的是null,这是因为第三个对象在方法中创建,是要进栈的,而revptrs
并不支持查找栈上的指针引用,所以需要换一个方法查找在Java Threads窗口选择main,然后点击打开Stack Memory窗口,就可以看到main线程中的栈内存内容,图中红框内就是第三个对象了,可以看到是引用了新生代中的
Hello$ObjectHolder
对象
总结:
JConsole:Java监视与管理控制台
JConsole(Java Monitoring and Management Console)是一款基于JMX(Java Manage-mentExtensions)的可视化监视、管理工具。它的主要功能是通过JMX的MBean(Managed Bean)对系统进行信息收集和参数动态调整
内存监控:
// VM参数:-Xms100m -Xmx100m -XX:+UseSerialGC
public class Hello {
/**
* 内存占位符对象,一个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);
}
}
run上述代码,然后在控制台执行命令:
jconsole
,选择对应的本地进程建立连接程序执行结束后,可以选择内存页签看到堆内存的变化。可以看到,Eden和Survivor区都差不多被清空,只有Old区还处于峰值状态,这是因为
List<OOMObject> list = new ArrayList<OOMObject>();
仍然存活未被回收,list对象在System.gc()执行时仍然处于作用域之内。如果把System.gc()移动到fillHeap()方法外调用就可以回收掉全部内存
线程监控:
- 线程资源等待演示:
public class Hello {
/**
* 线程死循环演示
*/
public static void createBusyThread() {
Thread thread = new Thread(() -> {
while (true)
;
}, "testBusyThread");
thread.start();
}
/**
* 线程锁等待演示
*/
public static void createLockThread(final Object lock) {
Thread thread = new Thread(() -> {
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线程。可以看到
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
一直在等待键盘输入,所以此时还在Runnable状态在控制台中输入任意内容,再次观察发现
testBusyThread
线程出现。根据堆栈跟踪可以看出,在代码13行处产生了资源等待将
createBusyThread();
注释掉,重新启动代码,和上述操作一致,观察testLockThread
线程。可以看到线程进入Waiting状态,在等待lock对象的notify()或notifyAll()方法的出现
- 线程死锁演示:
public class Hello {
/**
* 线程死锁等待演示
*/
static class SynAddRunalbe implements Runnable {
int a, b;
public SynAddRunalbe(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
synchronized (Integer.valueOf(a)) {
synchronized (Integer.valueOf(b)) {
System.out.println(a + b);
}
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(new SynAddRunalbe(1, 2)).start();
new Thread(new SynAddRunalbe(2, 1)).start();
}
}
}
这段代码开了200个线程去分别计算1+2以及2+1的值,理论上for循环都是可省略的,两个线程也可能会导致死锁,不过那样概率太小,需要尝试运行很多次才能看到死锁的效果。如果运气不是特别差的话,上面带for循环的版本最多运行两三次就会遇到线程死锁,程序无法结束。造成死锁的根本原因是Integer.valueOf()方法出于减少对象创建次数和节省内存的考虑,会对数值为-128~127之间的Integer对象进行缓存,如果valueOf()方法传入的参数在这个范围之内,就直接返回缓存中的对象。也就是说代码中尽管调用了200次Integer.valueOf()方法,但一共只返回了两个不同的Integer对象。假如某个线程的两个synchronized块之间发生了一次线程切换,那就会出现线程A在等待被线程B持有的Integer.valueOf(1),线程B又在等待被线程A持有的Integer.valueOf(2),结果大家都跑不下去的情况
- 启动代码,点击线程页签内的检测死锁,会发现新的页签。可以看到线程处于Blocked状态,
Thread48
和Thread49
互相持有了对方想要的资源无法释放
总结: