二、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ù)的概覽嚼隘,其中包括“對內存使用情況”袒餐、“線程”灸眼、“類”、“
CPU
使用情況”四種信息的曲線圖焰宣。
2.1.2 內存監(jiān)控
“內存”頁相當于可視化的jstat命令匕积,用于監(jiān)視受收集器管理的虛擬機內存(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
的“內存”頁進行監(jiān)視含鳞,曲線變化如上圖芹务。這里可以看到,內存池Eden
區(qū)的運行趨勢呈現(xiàn)折線狀潜必。監(jiān)視范圍擴大至整個堆后沃但,會發(fā)現(xiàn)曲線是一條向上增長的平滑曲線。之所以呈現(xiàn)折線是因為當Eden
區(qū)被填滿時進行一次GC
垂攘。從柱狀圖中可以看到,在1000
次循環(huán)執(zhí)行結束吱型,運行了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í)行時仍然處于作用域之內。如果把System.gc()
移動到fileHeap()
方法外調用就可以回收掉全部內存病往。
2.1.3 線程監(jiān)控
如果上面的“內存”頁簽相當于可視化的jstat
命令的話荣恐,“線程”頁簽的功能相當于可視化的jstack
命令车要,遇到線程停頓時可以使用這個頁簽進行監(jiān)控分析寿弱。之前說過線程長時間停頓的主要原因主要有:等待外部資源(數(shù)據(jù)庫連接嚷硫、網絡資源仔掸、設備資源等)医清、死循環(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()
方法被調用就會被激活,繼續(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
次就會遇到線程死鎖扔水,程序無法結束。造成死鎖的原因是Integer.valueOf()
方法基于減少對象創(chuàng)建次數(shù)和節(jié)省內存的考慮主届,[-128, 127]
之間的數(shù)字會被緩存待德,當valueOf()
方法傳入參數(shù)在這個范圍之內将宪,將直接返回緩存中的對象。即代碼中調用了200
次valueOf()
方法一共就只返回了兩個不同的對象印蔗。加入在某個線程的兩個synchronized
塊之間發(fā)生了一次線程切換燎潮,那就會出現(xiàn)線程A
等著被線程B
持有的Integer.valueOf(1)
,線程B
又等著被線程A
持有的Integer.valueOf(2)
除呵,結果出現(xiàn)大家都拋不下去的情景爪喘。
出現(xiàn)死鎖之后秉剑,點擊JConsole
線程面板的“監(jiān)測到死鎖”的按鈕,將出現(xiàn)一個新的“死鎖”頁簽诡曙,如圖所示略水。
從圖中可以看到渊涝,線程
Thread-43
在等待一個被線程Thread-12
持有的Integer
對象床嫌,而點擊線程Thread-12
則顯示它也在等待一個Integer
對象胸私,被線程Thread-43
持有岁疼,這樣就發(fā)生了死鎖。