二、JDK的可視化工具
2.1 JConsole:Java監(jiān)視與管理控制臺
JConsole(Java Monitoring and Management Console)是一種基于JMX的可視化監(jiān)視、管理工具。它管理部分的功能是針對JMX MBean進行管理,由于MBean可以使用代碼、中間件服務器的管理控制臺或者所有符合JMX規(guī)范的軟件進行訪問。
2.1.1 啟動JConsole
從JDK的bin目錄中可以直接雙擊運行此工具。

啟動后我們可以對其中一個進程進行監(jiān)控(這里我們只是啟動了
JConsole進程),得到的監(jiān)控界面如下:
說明:“概述”頁面顯示的是整個虛擬機主要運行數(shù)據(jù)的概覽,其中包括“對內(nèi)存使用情況”、“線程”、“類”、“
CPU使用情況”四種信息的曲線圖。
2.1.2 內(nèi)存監(jiān)控
“內(nèi)存”頁相當于可視化的jstat命令,用于監(jiān)視受收集器管理的虛擬機內(nèi)存(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堆中填充數(shù)據(jù),一共填充1000次,使用JConsole的“內(nèi)存”頁進行監(jiān)視,曲線變化如上圖。這里可以看到,內(nèi)存池Eden區(qū)的運行趨勢呈現(xiàn)折線狀。監(jiān)視范圍擴大至整個堆后,會發(fā)現(xiàn)曲線是一條向上增長的平滑曲線。之所以呈現(xiàn)折線是因為當Eden區(qū)被填滿時進行一次GC。從柱狀圖中可以看到,在1000次循環(huán)執(zhí)行結(jié)束,運行了Sytem.gc()后,雖然整個新生代Eden和Survivor區(qū)都基本被清空了,但是代表老年代的柱狀圖仍然保持峰值狀態(tài),說明被填充進堆中的數(shù)據(jù)在System.gc()方法執(zhí)行后仍然存活。
問題一:虛擬機啟動參數(shù)只限制了
Java堆為100MB,沒有指定-Xmn參數(shù)(-Xms初始堆大小,-Xmx最大堆大?。?,能否從監(jiān)控圖中估計出新生代有多大?
從圖中可以看到Eden空間為27328KB,因為沒有設置-XX:SurvivorRadio參數(shù),所以Eden與Survivor空間比例默認為8:1,整個新生代空間大約為27328KB*125%=34160KB。問題二、為何執(zhí)行了
System.gc()之后,圖中代表的老年代的柱狀圖仍然顯示峰值狀態(tài),代碼需要如何改動才能讓System.gc()回收掉填充到堆中的對象?
執(zhí)行完System.gc()后,空間未能回收是因為在List<OOMObject> list對象仍然存活,fileHeap()方法仍然沒有退出,因此list對象在System.gc()執(zhí)行時仍然處于作用域之內(nèi)。如果把System.gc()移動到fileHeap()方法外調(diào)用就可以回收掉全部內(nèi)存。
2.1.3 線程監(jiān)控
如果上面的“內(nèi)存”頁簽相當于可視化的jstat命令的話,“線程”頁簽的功能相當于可視化的jstack命令,遇到線程停頓時可以使用這個頁簽進行監(jiān)控分析。之前說過線程長時間停頓的主要原因主要有:等待外部資源(數(shù)據(jù)庫連接、網(wǎng)絡資源、設備資源等)、死循環(huán)、鎖等待(活鎖和死鎖)。下面通過代碼演示:
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狀態(tài),Runnable狀態(tài)的線程會被分配運行時間,但readBytes方法檢查到流沒有更新時會立刻歸還執(zhí)行令牌,這種等待只消耗很小的CPU資源。

接著監(jiān)控testBusyThread線程,如圖所示。此時處于一個死循環(huán)中,不會歸還線程執(zhí)行令牌,會消耗很多CPU資源。

在執(zhí)行testLockThread線程時,在等待著lock對象的nofify或notifyAll方法的出現(xiàn),此時線程處于WAITTING狀態(tài),在被喚醒之前是不會被分配執(zhí)行時間的。

這個線程只要
lock對象的notify()或notifyAll()方法被調(diào)用就會被激活,繼續(xù)執(zhí)行。下面的代碼演示了無法再被激活的死鎖等待。
//線程死鎖等待演示
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循環(huán)只需要運行2~3次就會遇到線程死鎖,程序無法結(jié)束。造成死鎖的原因是Integer.valueOf()方法基于減少對象創(chuàng)建次數(shù)和節(jié)省內(nèi)存的考慮,[-128, 127]之間的數(shù)字會被緩存,當valueOf()方法傳入?yún)?shù)在這個范圍之內(nèi),將直接返回緩存中的對象。即代碼中調(diào)用了200次valueOf()方法一共就只返回了兩個不同的對象。加入在某個線程的兩個synchronized塊之間發(fā)生了一次線程切換,那就會出現(xiàn)線程A等著被線程B持有的Integer.valueOf(1),線程B又等著被線程A持有的Integer.valueOf(2),結(jié)果出現(xiàn)大家都拋不下去的情景。
出現(xiàn)死鎖之后,點擊JConsole線程面板的“監(jiān)測到死鎖”的按鈕,將出現(xiàn)一個新的“死鎖”頁簽,如圖所示。

從圖中可以看到,線程
Thread-43在等待一個被線程Thread-12持有的Integer對象,而點擊線程Thread-12則顯示它也在等待一個Integer對象,被線程Thread-43持有,這樣就發(fā)生了死鎖。