写在前面的话
在很多大公司,对系统监控有较高的要求,也会自建很多监控平台。
在以前处理生产问题时,会用到jvm的一些工具,比如jstack命令,但是没有很系统,很多时候都是现用现学。所以打算抽时间整体梳理一下这块。在此声明一下,本文章参考周志明老师的《深入理解Java虚拟机》,如果感兴趣,可以购买一本,链接就不放了,以免有广告之嫌
传送门
JDK的可视化工具
JConsole
JConsole(Java Monitoring and Management Console)是jdk1.5就已经提供的虚拟机监控工具,是一种基于JMX的可视化监视、管理工具
启动JConsole
找到jdk的安装目录,打开jconsole工具,Windows是jconsole.exe(Linux可以执行/usr/libexec/java_home -V找到安装目录)
启动之后会自动搜索本机运行的所有虚拟机进程,不需要在用jps查询,选择其中一个进程即可开始监控。
远程的链接的下次再介绍,需要进行jvm配置
从上面可以看到2个进程,分别是JConsole、Idea。写一个testNg测试用例链接看一下界面
可以看到主界面包括"概览"、"内存"、"线程"、"类"、"VM概要"、"MBean"等6个页签。
其中"概述"的页签显示整个虚拟机的主要运行数据概览
内存监控
内存页签相当于jstat命令,用于监视受收集器管理的虚拟机内存(Java堆和永久代)的变化趋势。
通过一个例子来感受一下它的监视功能。运行之前先对虚拟机参数设置一
-Xms100m -Xmx100m -XX:+UseSerialGC
实例代码:以64KB/50毫秒的速度往Java堆中填充数据,一共填充1000次
public class Test1 {
/**
* 内存占用对象,一个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<>();
for (int i = 0; i < num; i++) {
Thread.sleep(100);
list.add(new OOMObject());
}
System.gc();
}
public static void main(String[] args) throws InterruptedException {
fillHeap(2000);
}
}
程序运行后,在"内存"页签中可以看到内存池Eden区的运行趋势呈折现状,扩大到整个堆后,曲线是一条向上的平滑曲线,直到OOM.
新生代内存
堆上内存
从Eden Space上面可以看到大小为27328KB,那整个新生代大小是多少呢?虚拟机启动参数没有设置-Xmn,默认Eden与Survivor为8:1,计算一下:27328/4*5=34160KB
线程监控
上面的"内存"页签相当于jstat的话,那么线程"线程页签"就相当于jstack,遇到线程停顿时可以使用这个页签进行监控分析。
线程长时间停顿的原因可能有:
- 等待外部资源:数据库连接、网络资源、设备资源等
- 死循环
- 锁等待:活锁、死锁
看一下代码例子
/**
* Alipay.com Inc. Copyright (c) 2004-2020 All Rights Reserved.
*/
package com.alipay.gmcore.enhancetest.testcase.fliggy.callback;
import java.io.BufferedReader;
import java.io.InputStreamReader;
/**
* @author laoke.tw
* @version $Id: Test2.java, v 0.1 2020-03-18 11:29 PM laoke.tw Exp $$
*/
public class Test2 {
/**
* 线程死循环演示
*/
public static void createBusyThread() {
Thread thread = new Thread(() -> {
while (true) {
}
}, "testBusyThread");
thread.start();
}
/**
* 线程锁等待演示
* @param lock
*/
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();
Object obj = new Object();
createLockThread(obj);
}
}
先监控main方法,这时线程为RUNNABLE状态,堆栈追踪显示BufferedInputStream在readBytes方法中等待System.in输入。
这种等待会消耗很小的CPU资源,因为虽然RUNNABLE状态的线程会被分配运行时间,但是readBytes方法在检测到没有流输入时,会立刻归还执行令牌。
接着在控制台输入一个值,然后监控:testBusyThread,发现也是RUNNABLE状态
堆栈追踪到代码20行,为while(true),这个时候会一直进行空循环直到线程切换,会消耗较多的CPU资源。
最后在看一下testLockThread线程,显示是WAITING状态,在等待lock对象通过notify或notifyAll唤醒。在这之前都不会分配时间片
注意到下面有一个"检测死锁"的按你,点击一下试试
发现未检测到死锁,表明这是一种活锁的等待状态,只要对象的notify或notifyAll方法被调用,这个线程就可以继续执行。
死锁等待
public class Test3 {
static class syncAddRunnable implements Runnable {
int a, b;
public syncAddRunnable(int a, int b) {
this.a = a;
this.b = b;
}
/**
* When an object implementing interface <code>Runnable</code> is used to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may take any action whatsoever.
*
* @see Thread#run()
*/
@Override
public void run() {
synchronized (Integer.valueOf(a)) {
synchronized (Integer.valueOf(b)) {
System.out.println(a + b);
}
}
}
}
public static void main(String[] args) throws Exception {
for (int i = 0, j = 1000; i < j; i++) {
new Thread(new syncAddRunnable(1, 2)).start();
new Thread(new syncAddRunnable(2, 1)).start();
}
}
}
执行一下上面这串代码,如无意外会出现下面这个死锁现象(看机器性能,如果for循环过少,也有可能看不到)
发现,Thread-590合Thread-591相互等待。
造成死锁的原因是Integer.valueOf()方法基于减少对象创建次数和节省内存的考虑,会将[-128,127]之间的数字缓存,当valueOf传入的参数在这个范围内,将之间返回缓存中的对象。也就是说代码调用了2000次Integer.valueOf()方法一共就返回了2个对象。
假如在某个线程的两个synchronized块之间发生了一次线程切换,那就会出现线程A等待线程B持有的Integer.valueOf(1),而线程B又等待线程A持有的Integer.valuefo(2),出现互相等待,导致大家都跑不出下去进而死锁