上节主要介绍JDK bin下的一些虚拟机性能监控与故障处理的工具,本节将介绍JDK的可视化工具JConsole。除了JConsole以外还有可视化工具Visual VM,这里就不在介绍了,可以参考书本。
JConsole(Java监视与管理的工具):直接执行bin下jconsole.exe后会自动搜索本级运行的所有虚拟机进行,不需要jps来查询。界面具体如下所示
进入之后可以看见概述、内存、线程、类、VM摘要以及MBean这六个标签。
这里介绍下其中3个标签内容(另外3个在以后了解其他知识的时候会介绍):
1.概述:显示的是整个虚拟机主要运行数据的概览,其中包括堆内存使用情况、线程、类、CPU使用情况4中信息的曲线图。
2.内存:相当于可视化的jstat命令,用于监视受收集器管理的虚拟机内存(JAVA堆和永久代)的变化趋势。
例子:这里执行书上一段例子来体验下这个监视功能。运行时,配置JVM参数-Xms100m -Xmx100m -XX:+UseSerialGC
运行代码:
public class JConsoleTest {
/**
*内存占位符对象,一个OOMObject大约占64KB
*/
static class OOMObject{
public byte[] placeholder = new byte[64*1024];
}
public static void fillHeap(int num)throws InterruptedException{
Listlist = new ArrayList();
for(int i=0;i
结果如图所示,Eden区呈现折线状,而整个堆是一条向上增长的平滑曲线。从右下角下面的图可以看出来,在1000次循环执行结束,运行gc后,整个新生代的Eden区会被清空,但是老年代仍保持峰值,这说明在gc后那些数据依旧存活着。这是为什么呢?首先这里先介绍下怎么计算新生代大小,这里的eden区的空间是27382KB,然后我们没有设置-XX:SurvivorRadio参数去配置Eden区和Survivor区的比例,那就是默认8:1,因此整个新生代的大小是27382/8*10=34227.5。好了,那么接下去,我们去看为什么gc后那些数据依旧存活着。在执行完gc后,list对象依然存活,fillHeap方法没有退出,因此list对象在gc执行时,仍然处于作用域之内,把gc移动到fillHeap之外即可全部回收掉内存。
3.线程:上面内存相当于jstat命令,这里就相当于jstack命令,遇到线程停顿时,可以在这里进行监控分析。下面通过例子来查看线程监控的变化。
public class JConsoleTestTwo {
/**
*线程死循环演示
*/
public static void createBusyThread(){
Thread thread = new Thread(new Runnable(){
@Override
public void run(){
while(true){ //16行
}
}
},"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 IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
br.readLine(); //44行
createBusyThread();
//br.readLine();
//Object obj = new Object();
//createLockThread(obj);
}
}
运行这个程序后,打开JConsole,可以看到如下图所示
根据堆栈跟踪,可以看到BufferedReader在readBytes方法中等待System.in的键盘输入,这时main线程为Runnable状态,Runnable状态的线程会被分配运行时间,但readBytes方法检查到流没有更新时会立刻归还执行令牌,这种等待只消耗很小的CPU资源。
接下去,我们键盘输入后,可以看到活动的线程数变了,线程中多了testBusyThread线程,我们监控testBusyThread看看,如下图所示。
testBusyThread线程一直在执行空循环,从堆栈追踪中看到一直在JConsoleTestTwo.java代码的16行停留,16行为:while(true)。 这时候线程为Runnable状态,而且没有归还线程执行令牌的动作,会在空循环上用尽全部执行时间直到线程切换,这种等待会消耗较多的CPU资源。
下面去掉注释,然后把testBusyThread方法进行注释,来看看活锁等待线程监控的效果,如下图所示
可以看出testLockThread线程在等待着lock对象的notify或者notifyAll。线程状态处于WAITING状态。这里的testLockThread还是处于活锁状态,当lock对象的notify或者notifyAll方法被执行之后,testLockThread线程就会被激活继续执行了,下面来看下死锁的例子,循环100次为了提高造成死锁的概率,代码具体不介绍了,应该都能看到,代码如下所示:
public class JConsoleTestThree {
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();
}
}
}
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);
}
}
}
}
执行这段代码后,在JConsole中点击检查死锁,可以看到如下图所示三个死锁的线程
上图很清晰地显示了线程Thread-168在等待一个被线程Thread-163持有Integer对象,而点击线程Thread-163则显示它也在等待一个被线程Thread-168持有Integer对象,这样就存在死锁了,都不存在等到锁释放的希望了。