JConsole
JConsole 是一个综合性图形界面监控工具,可以用曲线的形式监控各种数据,包括 MBean 中的属性值。
内存监控
“内存”页签的作用相当于可视化的jstat
命令,用于监视被收集器管理的虚拟机内存的变化趋势。
举例说明一下:
public class CommonMistakesApplication {
public static void main(String[] args) throws Exception {
fillHeap(1200);
}
/*** 内存占位符对象,一个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(200);
list.add(new OOMObject());
}
System.gc();
}
}
这段代码的作用是以64KB/50ms
的速度向Java堆中填充数据,一共填充1000次,使用JConsole 的“内存”页签进行监视,观察曲线和柱状指示图的变化。
设置虚拟机参数:
-Xms100m -Xmx100m -XX:+UseSerialGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails
整个堆内存趋势图
Eden Space 内存趋势图
Survivor Space 内存趋势图
Tenured Gen 内存趋势图
从上面的内存趋势图可以看出来,整个堆内存趋势图是一直平滑向上增长的,
而且在循环结束执行了System.gc()
后,虽然整个新生代Eden
和Survivor
区都基本被清空了,但是代表老年代的柱状图仍然保持峰值状态,说明被填充进堆中的数据在System.gc()
方法执行之后仍然存活。
为什么执行System.gc()
方法后,老年代的对象没有被回收掉呢?
原因是因为,在执行System.gc()
方法后,fillHeap()
方法仍然没有退出,list 对象
仍然处于作用域内,所以把System.gc()
移动到fillHeap()
方法外调用就可以回收掉全部内存。
回收结果如下图:
线程监控
“线程”页签的功能相当于可视化的jstack
命令,遇到线程停顿的时候可以使用这个页签的功能进行分析。
线程长时间停顿的主要原因有
等待外部资源(数据库连接、网络资源、设备资源等)、死循环、锁
等。
public class CommonMistakesApplication {
public static void main(String[] args) throws Exception {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
bufferedReader.readLine();
createBusyThread();
bufferedReader.readLine();
Object obj = new Object();
createLockThread(obj);
}
/**
* 测试死循环
*/
public static void createBusyThread() {
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getId());
while (true) ;
}, "testBusyThread");
thread.start();
}
/**
* 测试锁等待
* @param obj
*/
public static void createLockThread(final Object obj) {
Thread thread = new Thread(() -> {
synchronized (obj) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "testLockThread");
thread.start();
}
}
堆栈追踪显示
BufferedReader
的readBytes()
方法正在等待System.in
的键盘输入,这时候线程为Runnable
状态,Runnable
状态的线程仍会被分配运行时间,但readBytes()
方法检查到流没有更新就会立刻归还执行令牌给操作系统,这种等待只消耗很小的处理器资源。
在键盘上输入后,看 testBusyThread
线程的堆栈信息。
testBusyThread
线程一直在执行空循环,从堆栈追踪中看到一直在CommonMistakesApplication
代码的47行停留,47行的代码为while(true)
。
此时线程为Runnable
状态,而且没有归还线程执行令牌的动作,所以会在空循环耗尽操作系统分配给它的执行时间,直到线程切换为止,这种等待会消耗大量的处理器资源。
在键盘上输入后,看testLockThread
线程的堆栈信息。
testLockThread
线程在等待lock
对象的notify()
或notifyAll()
方法的出现,线程这时候处于WAITING
状态,在重新唤醒前不会被分配执行时间。
testLockThread
线程正处于正常的活锁等待中,只要lock
对象的notify()
或notifyAll()
方法被调用, 这个线程便能激活继续执行。
再看一个死锁等待的例子:
public class CommonMistakesApplication {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) throws Exception {
new Thread(() -> {
synchronized (lock1) {
try{
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {}
synchronized (lock2) {
System.out.println("thread1 over");
}
}
},"deadLock1").start();
new Thread(() -> {
synchronized (lock2) {
try{
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {}
synchronized (lock1) {
System.out.println("thread2 over");
}
}
},"deadLock2").start();
}
}
从死锁堆栈信息可以看出来,deadLock1 和 deadLock2 线程互相阻塞,都在等待获取对方所持有的锁。
jstat
如果没有条件使用图形界面(毕竟在 Linux 服务器上,我们主要使用命令行工具),又希望看到 GC 趋势的话,我们可以使用 jstat
工具。
通过 jstat -option
查看 jstat
有哪些操作:
- -class : 显示 ClassLoad 的相关信息;
- -compiler:显示 JIT 编译的相关信息;
- -gc:显示和 gc 相关的堆信息;
- -gccapacity:显示各个代的容量以及使用情况;
- -gcmetacapacity:显示 Metaspace 的大小;
- -gcnew:显示新生代信息;
- -gcnewcapacity:显示新生代大小和使用情况;
- -gcold:显示老年代和永久代的信息;
- -gcoldcapacity :显示老年代的大小;
- -gcutil:显示垃圾收集信息;
- -gccause:显示垃圾回收的相关信息(同 -gcutil),同时显示最后一次或当前正在发生的垃圾回收的诱因;
- -printcompilation:输出 JIT 编译的方法信息。
jstat
工具允许以固定的监控频次输出 JVM 的各种监控指标,比如使用 -gcutil
输出 GC 和内存占用汇总信息,每隔 3 秒输出一次,输出 100 次。
S0 表示 Survivor0 区占用百分比,S1 表示 Survivor1 区占用百分比,E 表示 Eden 区占用百分比,O 表示老年代占用百分比,M 表示元数据区占用百分比,YGC 表示年轻代回收次数,YGCT 表示年轻代回收耗时,FGC 表示老年代回收次数,FGCT 表示老年代回收耗时。
jstack
jstack
是一种线程堆栈分析工具,最常用的功能就是使用 jstack pid
命令查看线程的堆栈信息,通常会结合 top -p pid -H
或 pidstat -p pid -t
一起查看具体线程的状态,也经常用来排查一些死锁的异常。
比如:上面那个死锁的代码,就可以用 jstack
打印出堆栈信息。