二、JDK的可视化工具
2.1 JConsole:Java监视与管理控制台
JConsole(Java Monitoring and Management Console)
是一种基于JMX
的可视化监视、管理工具。它管理部分的功能是针对JMX MBean
进行管理,由于MBean
可以使用代码、中间件服务器的管理控制台或者所有符合JMX
规范的软件进行访问。
2.1.1 启动JConsole
从JDK
的bin
目录中可以直接双击运行此工具。
启动后我们可以对其中一个进程进行监控(这里我们只是启动了
JConsole
进程),得到的监控界面如下:说明:“概述”页面显示的是整个虚拟机主要运行数据的概览,其中包括“对内存使用情况”、“线程”、“类”、“
CPU
使用情况”四种信息的曲线图。
2.1.2 内存监控
“内存”页相当于可视化的jstat命令,用于监视受收集器管理的虚拟机内存(Java
堆和永久代)的变化趋势。这里通过例子说明(这里亲自试验了书中的例子,但是拿到的曲线和书中有点不一样,这里还是以书中为准):
代码如下:
//使用java -Xms100m -Xmx100m -XX:+UseSerialGC运行
public static void main(String[] args) throws Exception{
fileHeap(1000);
}
static class OOMObject{
public byte[] placeholder = new byte[64 * 1024];
}
public static void fileHeap(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();
}
说明:这段代码的作用是以
64KB/50ms
的速度往Java
堆中填充数据,一共填充1000
次,使用JConsole
的“内存”页进行监视,曲线变化如上图。这里可以看到,内存池Eden
区的运行趋势呈现折线状。监视范围扩大至整个堆后,会发现曲线是一条向上增长的平滑曲线。之所以呈现折线是因为当Eden
区被填满时进行一次GC
。从柱状图中可以看到,在1000
次循环执行结束,运行了Sytem.gc()
后,虽然整个新生代Eden
和Survivor
区都基本被清空了,但是代表老年代的柱状图仍然保持峰值状态,说明被填充进堆中的数据在System.gc()
方法执行后仍然存活。
问题一:虚拟机启动参数只限制了
Java
堆为100MB
,没有指定-Xmn
参数(-Xms
初始堆大小,-Xmx
最大堆大小),能否从监控图中估计出新生代有多大?
从图中可以看到Eden
空间为27328KB
,因为没有设置-XX:SurvivorRadio
参数,所以Eden
与Survivor
空间比例默认为8:1
,整个新生代空间大约为27328KB*125%=34160KB
。问题二、为何执行了
System.gc()
之后,图中代表的老年代的柱状图仍然显示峰值状态,代码需要如何改动才能让System.gc()
回收掉填充到堆中的对象?
执行完System.gc()
后,空间未能回收是因为在List<OOMObject> list
对象仍然存活,fileHeap()
方法仍然没有退出,因此list
对象在System.gc()
执行时仍然处于作用域之内。如果把System.gc()
移动到fileHeap()
方法外调用就可以回收掉全部内存。
2.1.3 线程监控
如果上面的“内存”页签相当于可视化的jstat
命令的话,“线程”页签的功能相当于可视化的jstack
命令,遇到线程停顿时可以使用这个页签进行监控分析。之前说过线程长时间停顿的主要原因主要有:等待外部资源(数据库连接、网络资源、设备资源等)、死循环、锁等待(活锁和死锁)。下面通过代码演示:
public static void createBusyThread(){
Thread thread = new Thread(new Runnable(){
@Override
public void run(){
while(true)
;
}
}, "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){
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
br.readLine();
createBusyThread();
br.readLine();
Object obj = new Object();
createLockThread(obj);
}
说明:程序运行后,首先在“线程”页签中选择main
线程,如图所示。堆栈追踪显示BufferedReader
在readBytes
方法中等待System.in
的键盘输入,这时线程为Runnable
状态,Runnable
状态的线程会被分配运行时间,但readBytes
方法检查到流没有更新时会立刻归还执行令牌,这种等待只消耗很小的CPU
资源。
接着监控testBusyThread
线程,如图所示。此时处于一个死循环中,不会归还线程执行令牌,会消耗很多CPU
资源。
在执行testLockThread
线程时,在等待着lock
对象的nofify
或notifyAll
方法的出现,此时线程处于WAITTING
状态,在被唤醒之前是不会被分配执行时间的。
这个线程只要
lock
对象的notify()
或notifyAll()
方法被调用就会被激活,继续执行。下面的代码演示了无法再被激活的死锁等待。
//线程死锁等待演示
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
循环只需要运行2~3
次就会遇到线程死锁,程序无法结束。造成死锁的原因是Integer.valueOf()
方法基于减少对象创建次数和节省内存的考虑,[-128, 127]
之间的数字会被缓存,当valueOf()
方法传入参数在这个范围之内,将直接返回缓存中的对象。即代码中调用了200
次valueOf()
方法一共就只返回了两个不同的对象。加入在某个线程的两个synchronized
块之间发生了一次线程切换,那就会出现线程A
等着被线程B
持有的Integer.valueOf(1)
,线程B
又等着被线程A
持有的Integer.valueOf(2)
,结果出现大家都抛不下去的情景。
出现死锁之后,点击JConsole
线程面板的“监测到死锁”的按钮,将出现一个新的“死锁”页签,如图所示。
从图中可以看到,线程
Thread-43
在等待一个被线程Thread-12
持有的Integer
对象,而点击线程Thread-12
则显示它也在等待一个Integer
对象,被线程Thread-43
持有,这样就发生了死锁。