VisualVM 是一款免費(fèi)的蠢护,集成了多個(gè) JDK 命令行工具的可視化工具惕蹄,它能為您提供強(qiáng)大的分析能力绎狭,對 Java 應(yīng)用程序做性能分析和調(diào)優(yōu)。這些功能包括生成和分析海量數(shù)據(jù)侥涵、跟蹤內(nèi)存泄漏沼撕、監(jiān)控垃圾回收器、執(zhí)行內(nèi)存和 CPU 分析芜飘,同時(shí)它還支持在 MBeans 上進(jìn)行瀏覽和操作务豺。本文主要介紹如何使用 VisualVM 進(jìn)行性能分析及調(diào)優(yōu)。
目錄:
? 準(zhǔn)備工作
? 內(nèi)存分析篇
? ? 內(nèi)存堆Heap
? ? 永久保留區(qū)域PermGen
? CPU分析篇
? 線程分析篇
? 參考文獻(xiàn)
準(zhǔn)備工作
自從 JDK 6 Update 7 以后已經(jīng)作為 Oracle JDK 的一部分嗦明,位于 JDK 根目錄的 bin 文件夾下笼沥,無需安裝,直接運(yùn)行即可娶牌。
內(nèi)存分析篇
VisualVM 通過檢測 JVM 中加載的類和對象信息等幫助我們分析內(nèi)存使用情況敬拓,我們可以通過 VisualVM 的監(jiān)視標(biāo)簽對應(yīng)用程序進(jìn)行內(nèi)存分析。
1)內(nèi)存堆Heap
首先我們來看內(nèi)存堆Heap使用情況裙戏,我本機(jī)eclipse的進(jìn)程在visualVM顯示如下:
隨便寫個(gè)小程序占用內(nèi)存大的,運(yùn)行一下
程序如下:
package jvisualVM;
public class JavaHeapTest {
? ? public final static int OUTOFMEMORY = 200000000;
? ? private String oom;
? ? private int length;
? ? StringBuffer tempOOM = new StringBuffer();
? ? public JavaHeapTest(int leng) {
? ? ? ? this.length = leng;
? ? ? ? int i = 0;
? ? ? ? while (i < leng) {
? ? ? ? ? ? i++;
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? tempOOM.append("a");
? ? ? ? ? ? } catch (OutOfMemoryError e) {
? ? ? ? ? ? ? e.printStackTrace();
? ? ? ? ? ? ? break;
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? this.oom = tempOOM.toString();
? ? }
? ? public String getOom() {
? ? ? ? return oom;
? ? }
? ? public int getLength() {
? ? ? ? return length;
? ? }
? ? public static void main(String[] args) {
? ? ? ? JavaHeapTest javaHeapTest = new JavaHeapTest(OUTOFMEMORY);
? ? ? ? System.out.println(javaHeapTest.getOom().length());
? ? }
}
查看VisualVM Monitor tab, 堆內(nèi)存變大了
在程序運(yùn)行結(jié)束之前厕诡, 點(diǎn)擊Heap Dump 按鈕累榜, 等待一會(huì)兒,得到dump結(jié)果灵嫌,可以看到一些Summary信息
點(diǎn)擊Classes壹罚, 發(fā)現(xiàn)char[]所占用的內(nèi)存是最大的
雙擊它,得到如下Instances結(jié)果
Instances是按Size由大到小排列的
第一個(gè)就是最大的寿羞, 展開Field區(qū)域的 values
StringBuffer類型的 全局變量 tempOOM 占用內(nèi)存特別大猖凛, 注意局部變量是無法通過 堆dump來得到分析結(jié)果的。
另外绪穆,對于“堆 dump”來說辨泳,在遠(yuǎn)程監(jiān)控jvm的時(shí)候虱岂,VisualVM是沒有這個(gè)功能的,只有本地監(jiān)控的時(shí)候才有菠红。
2)永久保留區(qū)域PermGen
其次來看下永久保留區(qū)域PermGen使用情況
運(yùn)行一段類加載的程序第岖,代碼如下:
package jvisualVM;
import java.io.File;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
public class TestPermGen {
? ? private static List
? ? public static void main(String[] args) throws Exception {
? ? ? ? permLeak();
? ? }
? ? private static void permLeak() throws Exception {
? ? ? ? for (int i = 0; i < 1000; i++) {
? ? ? ? ? ? URL[] urls = getURLS();
? ? ? ? ? ? URLClassLoader urlClassloader = new URLClassLoader(urls, null);
? ? ? ? ? ? Class logfClass = Class.forName("org.apache.commons.logging.LogFactory", true,urlClassloader);
? ? ? ? ? ? Method getLog = logfClass.getMethod("getLog", String.class);
? ? ? ? ? ? Object result = getLog.invoke(logfClass, "TestPermGen");
? ? ? ? ? ? insList.add(result);
? ? ? ? ? ? System.out.println(i + ": " + result);
? ? ? ? }
? ? }
? ? private static URL[] getURLS() throws MalformedURLException {
? ? ? ? File libDir = new File("C:/Users/wadexu/.m2/repository/commons-logging/commons-logging/1.1.1");
? ? ? ? File[] subFiles = libDir.listFiles();
? ? ? ? int count = subFiles.length;
? ? ? ? URL[] urls = new URL[count];
? ? ? ? for (int i = 0; i < count; i++) {
? ? ? ? ? ? urls[i] = subFiles[i].toURI().toURL();
? ? ? ? }
? ? ? ? return urls;
? ? }
一個(gè)類型裝載之后會(huì)創(chuàng)建一個(gè)對應(yīng)的java.lang.Class實(shí)例,這個(gè)實(shí)例本身和普通對象實(shí)例一樣存儲(chǔ)于堆中试溯,我覺得之所以說是這是一種特殊的實(shí)例蔑滓,某種程度上是因?yàn)槠涑洚?dāng)了訪問PermGen區(qū)域中類型信息的代理者。
運(yùn)行一段時(shí)間后拋OutOfMemoryError了遇绞, VisualVM監(jiān)控結(jié)果如下:
結(jié)論:PermGen區(qū)域分配的堆空間過小键袱,我們可以通過設(shè)置-XX: PermSize參數(shù)和-XX:MaxPermSize參數(shù)來解決。
關(guān)于PermGen OOM深入分析請參考這篇文章
關(guān)于Perform GC, 請參考這篇文章
這部分知識還是比較深入的摹闽,有空還要繼續(xù)研究蹄咖。
CPU分析篇
CPU 性能分析的主要目的是統(tǒng)計(jì)函數(shù)的調(diào)用情況及執(zhí)行時(shí)間,或者更簡單的情況就是統(tǒng)計(jì)應(yīng)用程序的 CPU 使用情況钩骇。
沒有程序運(yùn)行時(shí)的 CPU 使用情況如下圖:
運(yùn)行一段 占用CPU 的小程序比藻,代碼如下
package jvisualVM;
public class MemoryCpuTest {
? ? public static void main(String[] args) throws InterruptedException {
? ? ? ? cpuFix();
? ? }
? ? /**
? ? * cpu 運(yùn)行固定百分比
? ? *
* @throws InterruptedException
*/
public static void cpuFix() throws InterruptedException {
// 80%的占有率
int busyTime = 8;
// 20%的占有率
int idelTime = 2;
// 開始時(shí)間
long startTime = 0;
while (true) {
// 開始時(shí)間
startTime = System.currentTimeMillis();
/*
* 運(yùn)行時(shí)間
*/
while (System.currentTimeMillis() - startTime < busyTime) {
;
}
// 休息時(shí)間
Thread.sleep(idelTime);
}
}
}
查看監(jiān)視頁面 Monitor tab
過高的 CPU 使用率可能是由于我們的項(xiàng)目中存在低效的代碼;
在我們對程序施壓的時(shí)候倘屹,過低的 CPU 使用率也有可能是程序的問題银亲。
點(diǎn)擊取樣器Sampler, 點(diǎn)擊“CPU”按鈕纽匙, 啟動(dòng)CPU性能分析會(huì)話务蝠,VisualVM 會(huì)檢測應(yīng)用程序所有的被調(diào)用的方法,
在CPU samples tab 下可以看到我們的方法cpufix() 的自用時(shí)間最長烛缔, 如下圖:
切換到Thread CPU Time 頁面下馏段,我們的 main 函數(shù)這個(gè)進(jìn)程 占用CPU時(shí)間最長, 如下圖:
線程分析篇
Java 語言能夠很好的實(shí)現(xiàn)多線程應(yīng)用程序践瓷。當(dāng)我們對一個(gè)多線程應(yīng)用程序進(jìn)行調(diào)試或者開發(fā)后期做性能調(diào)優(yōu)的時(shí)候院喜,往往需要了解當(dāng)前程序中所有線程的運(yùn)行狀態(tài),是否有死鎖晕翠、熱鎖等情況的發(fā)生喷舀,從而分析系統(tǒng)可能存在的問題。
在 VisualVM 的監(jiān)視標(biāo)簽內(nèi)淋肾,我們可以查看當(dāng)前應(yīng)用程序中所有活動(dòng)線程(Live threads)和守護(hù)線程(Daemon threads)的數(shù)量等實(shí)時(shí)信息硫麻。
運(yùn)行一段小程序,代碼如下:
package jvisualVM;
public class MyThread extends Thread{
public static void main(String[] args) {
MyThread mt1 = new MyThread("Thread a");
MyThread mt2 = new MyThread("Thread b");
mt1.setName("My-Thread-1 ");
mt2.setName("My-Thread-2 ");
mt1.start();
mt2.start();
}
public MyThread(String name) {
}
? ? public void run() {
while (true) {
}
}
}
Live threads 從11增加兩個(gè) 變成13了
Daemon threads從8增加兩個(gè) 變成10了
VisualVM 的線程標(biāo)簽提供了三種視圖樊卓,默認(rèn)會(huì)以時(shí)間線的方式展現(xiàn)拿愧, 如下圖:
可以看到兩個(gè)我們r(jià)un的程序里啟的線程:My-Thread-1 和 My-Thread-2
另外還有兩種視圖分別是表視圖和詳細(xì)信息視圖, 這里看一下每個(gè)Thread的詳細(xì)視圖:
再來一段死鎖的程序碌尔,看VisualVM 能否分析出來
package jvisualVM;
public class DeadLock {
? ? public static void main(String[] args) {
? ? ? ? Resource r1 = new Resource();
? ? ? ? Resource r0 = new Resource();
? ? ? ? Thread myTh1 = new LockThread1(r1, r0);
? ? ? ? Thread myTh0 = new LockThread0(r1, r0);
? ? ? ? myTh1.setName("DeadLock-1 ");
? ? ? ? myTh0.setName("DeadLock-0 ");
? ? ? ? myTh1.start();
? ? ? ? myTh0.start();
? ? }
}
? ? class Resource {
? ? ? ? private int i;
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
}
? ? class LockThread1 extends Thread {
? ? ? ? private Resource r1, r2;
public LockThread1(Resource r1, Resource r2) {
this.r1 = r1;
this.r2 = r2;
}
@Override
public void run() {
int j = 0;
while (true) {
synchronized (r1) {
System.out.println("The first thread got r1's lock " + j);
synchronized (r2) {
System.out.println("The first thread got r2's lock? " + j);
}
}
j++;
}
}
}
? ? class LockThread0 extends Thread {
? ? ? ? private Resource r1, r2;
public LockThread0(Resource r1, Resource r2) {
this.r1 = r1;
this.r2 = r2;
}
@Override
public void run() {
int j = 0;
while (true) {
synchronized (r2) {
System.out.println("The second thread got r2's lock? " + j);
synchronized (r1) {
System.out.println("The second thread got r1's lock" + j);
}
}
j++;
}
}
}
打開VisualVM檢測到的JVM進(jìn)程,我們可以看到這個(gè)tab在閃,VisualVM已經(jīng)檢測到我這個(gè)package下面的DeadLock類出錯(cuò)了
切換到Thread tab铣卡, 可以看到死鎖了, Deadlock detected!
另外可以點(diǎn)擊Thread Dump 線程轉(zhuǎn)儲(chǔ)陪白,進(jìn)一步分析,在這里就不贅述了膳灶,有興趣的讀者可以自行實(shí)驗(yàn)咱士。