VisualVM 是一款免費的,集成了多個 JDK 命令行工具的可視化工具,它能為您提供強大的分析能力,對 Java 應用程序做性能分析和調優。這些功能包括生成和分析海量數據、跟蹤內存泄漏、監控垃圾回收器、執行內存和 CPU 分析,同時它還支持在 MBeans 上進行瀏覽和操作。本文主要介紹如何使用 VisualVM 進行性能分析及調優。
目錄:
? 準備工作
? 內存分析篇
? ? 內存堆Heap
? ? 永久保留區域PermGen
? CPU分析篇
? 線程分析篇
? 參考文獻
準備工作
自從 JDK 6 Update 7 以后已經作為 Oracle JDK 的一部分,位于 JDK 根目錄的 bin 文件夾下,無需安裝,直接運行即可。
內存分析篇
VisualVM 通過檢測 JVM 中加載的類和對象信息等幫助我們分析內存使用情況,我們可以通過 VisualVM 的監視標簽對應用程序進行內存分析。
1)內存堆Heap
首先我們來看內存堆Heap使用情況,我本機eclipse的進程在visualVM顯示如下:
隨便寫個小程序占用內存大的,運行一下
程序如下:
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, 堆內存變大了
在程序運行結束之前, 點擊Heap Dump 按鈕, 等待一會兒,得到dump結果,可以看到一些Summary信息
點擊Classes, 發現char[]所占用的內存是最大的
雙擊它,得到如下Instances結果
Instances是按Size由大到小排列的
第一個就是最大的, 展開Field區域的 values
StringBuffer類型的 全局變量 tempOOM 占用內存特別大, 注意局部變量是無法通過 堆dump來得到分析結果的。
另外,對于“堆 dump”來說,在遠程監控jvm的時候,VisualVM是沒有這個功能的,只有本地監控的時候才有。
2)永久保留區域PermGen
其次來看下永久保留區域PermGen使用情況
運行一段類加載的程序,代碼如下:
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;
? ? }
一個類型裝載之后會創建一個對應的java.lang.Class實例,這個實例本身和普通對象實例一樣存儲于堆中,我覺得之所以說是這是一種特殊的實例,某種程度上是因為其充當了訪問PermGen區域中類型信息的代理者。
運行一段時間后拋OutOfMemoryError了, VisualVM監控結果如下:
結論:PermGen區域分配的堆空間過小,我們可以通過設置-XX: PermSize參數和-XX:MaxPermSize參數來解決。
關于PermGen OOM深入分析請參考這篇文章
關于Perform GC, 請參考這篇文章
這部分知識還是比較深入的,有空還要繼續研究。
CPU分析篇
CPU 性能分析的主要目的是統計函數的調用情況及執行時間,或者更簡單的情況就是統計應用程序的 CPU 使用情況。
沒有程序運行時的 CPU 使用情況如下圖:
運行一段 占用CPU 的小程序,代碼如下
package jvisualVM;
public class MemoryCpuTest {
? ? public static void main(String[] args) throws InterruptedException {
? ? ? ? cpuFix();
? ? }
? ? /**
? ? * cpu 運行固定百分比
? ? *
* @throws InterruptedException
*/
public static void cpuFix() throws InterruptedException {
// 80%的占有率
int busyTime = 8;
// 20%的占有率
int idelTime = 2;
// 開始時間
long startTime = 0;
while (true) {
// 開始時間
startTime = System.currentTimeMillis();
/*
* 運行時間
*/
while (System.currentTimeMillis() - startTime < busyTime) {
;
}
// 休息時間
Thread.sleep(idelTime);
}
}
}
查看監視頁面 Monitor tab
過高的 CPU 使用率可能是由于我們的項目中存在低效的代碼;
在我們對程序施壓的時候,過低的 CPU 使用率也有可能是程序的問題。
點擊取樣器Sampler, 點擊“CPU”按鈕, 啟動CPU性能分析會話,VisualVM 會檢測應用程序所有的被調用的方法,
在CPU samples tab 下可以看到我們的方法cpufix() 的自用時間最長, 如下圖:
切換到Thread CPU Time 頁面下,我們的 main 函數這個進程 占用CPU時間最長, 如下圖:
線程分析篇
Java 語言能夠很好的實現多線程應用程序。當我們對一個多線程應用程序進行調試或者開發后期做性能調優的時候,往往需要了解當前程序中所有線程的運行狀態,是否有死鎖、熱鎖等情況的發生,從而分析系統可能存在的問題。
在 VisualVM 的監視標簽內,我們可以查看當前應用程序中所有活動線程(Live threads)和守護線程(Daemon threads)的數量等實時信息。
運行一段小程序,代碼如下:
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增加兩個 變成13了
Daemon threads從8增加兩個 變成10了
VisualVM 的線程標簽提供了三種視圖,默認會以時間線的方式展現, 如下圖:
可以看到兩個我們run的程序里啟的線程:My-Thread-1 和 My-Thread-2
另外還有兩種視圖分別是表視圖和詳細信息視圖, 這里看一下每個Thread的詳細視圖:
再來一段死鎖的程序,看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進程,我們可以看到這個tab在閃,VisualVM已經檢測到我這個package下面的DeadLock類出錯了
切換到Thread tab, 可以看到死鎖了, Deadlock detected!
另外可以點擊Thread Dump 線程轉儲,進一步分析,在這里就不贅述了,有興趣的讀者可以自行實驗。