吃透 JVM 診斷方法與工具使用

威哥爱编程發表於2024-08-01

JVM(Java虛擬機器)是Java程式執行的基礎環境,它提供了記憶體管理、執行緒管理和效能監控等功能。吃透JVM診斷方法,可以幫助開發者更有效地解決Java應用在執行時遇到的問題。以下是一些常見的JVM診斷方法:

  1. 使用JConsole:

    • JConsole是一個視覺化監控工具,可以連線到本地或遠端的JVM例項,檢視記憶體使用情況、執行緒狀態、類載入資訊等。
  2. 使用VisualVM:

    • VisualVM提供了更豐富的功能,包括執行緒分析、記憶體洩漏分析、GC日誌分析等。
  3. 使用jstack:

    • jstack是一個命令列工具,可以生成Java執行緒的快照,用於分析執行緒的狀態和死鎖問題。
  4. 使用jmap:

    • jmap可以用來生成堆轉儲快照(heap dump),分析記憶體使用情況,查詢記憶體洩漏。
  5. 使用jstat:

    • jstat提供了執行中的JVM例項的效能資料,包括類載入、記憶體、垃圾回收等統計資訊。
  6. 使用jcmd:

    • jcmd是一個多功能命令列工具,可以執行各種診斷命令,如獲取執行緒棧、記憶體資訊等。
  7. 分析GC日誌:

    • 垃圾收集器(GC)的日誌包含了垃圾回收的詳細資訊,透過分析這些日誌可以瞭解GC的行為和效能瓶頸。
  8. 使用MAT(Memory Analyzer Tool):

    • MAT是一個強大的堆轉儲分析工具,可以幫助開發者分析記憶體使用情況,查詢記憶體洩漏。
  9. 使用Profilers:

    • 使用效能分析工具(如YourKit, JProfiler)可以幫助開發者瞭解應用程式的效能瓶頸。

透過這些方法,你可以更深入地瞭解JVM的內部工作機制,從而更有效地診斷和解決Java應用中的問題。下面 V 哥一一來講解使用方法。

1. 使用JConsole

模擬示例程式碼來演示JConsole工具的使用,我們可以建立一個簡單的Java應用程式,它將展示記憶體使用、執行緒監控和GC活動。然後,我們將使用JConsole來監控這個應用程式。

示例Java應用程式程式碼

import java.util.ArrayList;
import java.util.List;

public class JConsoleDemo {
    private static final int LIST_SIZE = 1000;
    private static List<Object> list = new ArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        // 模擬記憶體使用增長
        for (int i = 0; i < 5; i++) {
            list.add(new byte[1024 * 1024]); // 新增1MB資料
            Thread.sleep(1000); // 模擬延遲
            System.out.println("Memory used: " + (i + 1) + "MB");
        }

        // 模擬執行緒活動
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("Thread 1 is running");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            while (true) {
                synchronized (JConsoleDemo.class) {
                    System.out.println("Thread 2 is running");
                }
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });

        thread1.start();
        thread2.start();

        // 模擬GC活動
        Runtime.getRuntime().gc();
    }
}

使用JConsole監控示例應用程式

  1. 編譯並執行示例應用程式

    • 使用javac JConsoleDemo.java編譯Java程式碼。
    • 使用java -classpath . JConsoleDemo執行應用程式。
  2. 啟動JConsole

    • 在命令列中輸入jconsole並回車。
  3. 連線到應用程式

    • 在JConsole中,選擇"連線",然後從列表中選擇正在執行的JConsoleDemo應用程式。
  4. 監控記憶體使用

    • 在"記憶體"標籤頁中,觀察堆記憶體的變化。你應該能看到隨著程式執行,記憶體使用量逐漸增加。
  5. 監控執行緒狀態

    • 切換到"執行緒"標籤頁,檢視執行緒的活動。注意執行緒1和執行緒2的執行情況。
  6. 分析執行緒死鎖

    • 如果執行緒2在同步塊中等待,而執行緒1嘗試獲取同一個鎖,這將導致死鎖。使用"Find Deadlocked Threads"功能來檢測。
  7. 監控GC活動

    • 回到"記憶體"標籤頁,檢視GC的統計資訊,如GC次數和GC時間。
  8. 生成堆轉儲

    • 如果需要進一步分析記憶體使用情況,可以在"記憶體"標籤頁中使用"Dump Heap"功能生成堆轉儲。
  9. 監控MBeans

    • 如果應用程式註冊了自定義MBeans,可以在"MBeans"標籤頁中檢視它們。

透過這個示例,你可以瞭解如何使用JConsole來監控Java應用程式的記憶體使用、執行緒狀態和GC活動。這些資訊對於診斷效能問題和最佳化應用程式至關重要。

2. 使用VisualVM

VisualVM是一個強大的多合一工具,它提供了對Java應用程式的深入分析,包括CPU、記憶體、執行緒和GC等。下面是一個簡單的Java應用程式示例,它將展示如何使用VisualVM來監控和分析。

示例Java應用程式程式碼

public class VisualVMDemo {
    private static final int ARRAY_SIZE = 1000;
    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        // 建立一個大陣列以模擬記憶體使用
        Object[] largeArray = new Object[ARRAY_SIZE];

        // 建立執行緒以模擬CPU使用和執行緒活動
        Thread cpuIntensiveThread = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                // 模擬CPU密集型任務
                for (int j = 0; j < 100000; j++) {
                    // 空迴圈體
                }
            }
        });

        // 建立執行緒以模擬執行緒死鎖
        Thread thread1 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("Thread 1 acquired lock");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                synchronized (VisualVMDemo.class) {
                    System.out.println("Thread 1 acquired second lock");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (VisualVMDemo.class) {
                System.out.println("Thread 2 acquired second lock");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                synchronized (lock) {
                    System.out.println("Thread 2 acquired lock");
                }
            }
        });

        // 啟動執行緒
        cpuIntensiveThread.start();
        thread1.start();
        thread2.start();

        // 模擬記憶體洩漏
        while (true) {
            // 持續建立物件但不釋放引用
            largeArray[(int) (Math.random() * ARRAY_SIZE)] = new Object();
            Thread.sleep(10);
        }
    }
}

使用VisualVM監控示例應用程式

  1. 編譯並執行示例應用程式

    • 使用javac VisualVMDemo.java編譯Java程式碼。
    • 使用java -classpath . VisualVMDemo執行應用程式。
  2. 啟動VisualVM

    • 在命令列中輸入visualvm並回車。
  3. 連線到應用程式

    • VisualVM會自動檢測到執行中的Java應用程式。如果沒有自動檢測到,你可以使用"新增JMX連線"手動新增。
  4. 監控CPU使用

    • 在"監視"選項卡中,檢視CPU的"當前"和"歷史"使用情況。
  5. 監控記憶體使用

    • 在"監視"選項卡中,檢視堆記憶體和非堆記憶體的使用情況。
  6. 分析記憶體洩漏

    • 使用"記憶體"選項卡,點選"GC"按鈕來觸發垃圾回收,然後觀察是否有物件沒有被回收,這可能表明記憶體洩漏。
  7. 分析執行緒死鎖

    • 在"執行緒"選項卡中,查詢死鎖的執行緒。VisualVM會顯示死鎖的執行緒和它們的呼叫棧。
  8. 分析GC活動

    • 在"監視"選項卡中,檢視GC的統計資訊,如GC次數、GC持續時間等。
  9. 生成堆轉儲

    • 在"記憶體"選項卡中,點選"堆轉儲"按鈕來生成堆轉儲檔案,然後使用分析工具進一步分析。
  10. 分析取樣CPU Profile

    • 在"CPU"選項卡中,啟動CPU分析器,檢視哪些方法佔用了最多的CPU時間。
  11. 檢視應用程式的類載入資訊

    • 在"類"選項卡中,檢視已載入的類和它們的載入時間。

透過這個示例,你可以瞭解VisualVM的多種功能,包括CPU分析、記憶體分析、執行緒分析和GC分析等。這些工具可以幫助你診斷和最佳化Java應用程式的效能問題。

3. 使用jstack

jstack是一個命令列工具,它用於生成Java執行緒的堆疊跟蹤,這對於分析執行緒狀態和死鎖問題非常有用。下面是一個簡單的Java應用程式示例,它將演示如何使用jstack來獲取執行緒的堆疊跟蹤。

示例Java應用程式程式碼

public class JStackDemo {
    public static void main(String[] args) {
        // 建立一個示例物件,用於在堆疊跟蹤中識別
        Object exampleObject = new Object();

        // 建立兩個執行緒,它們將嘗試獲取同一個鎖,導致死鎖
        Thread thread1 = new Thread(new DeadlockDemo("Thread-1", exampleObject, true));
        Thread thread2 = new Thread(new DeadlockDemo("Thread-2", exampleObject, false));

        thread1.start();
        thread2.start();
    }
}

class DeadlockDemo implements Runnable {
    private final String name;
    private final Object lock1;
    private final boolean lockOrder;

    public DeadlockDemo(String name, Object lock1, boolean lockOrder) {
        this.name = name;
        this.lock1 = lock1;
        this.lockOrder = lockOrder;
    }

    @Override
    public void run() {
        System.out.println(name + " started");

        if (lockOrder) {
            synchronized (lock1) {
                System.out.println(name + " acquired lock1");
                try {
                    Thread.sleep(500); // 模擬工作
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                synchronized (JStackDemo.class) {
                    System.out.println(name + " acquired lock2");
                }
            }
        } else {
            synchronized (JStackDemo.class) {
                System.out.println(name + " acquired lock2");
                try {
                    Thread.sleep(500); // 模擬工作
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                synchronized (lock1) {
                    System.out.println(name + " acquired lock1");
                }
            }
        }
    }
}

使用jstack獲取執行緒堆疊跟蹤

  1. 編譯並執行示例應用程式

    • 使用javac JStackDemo.java編譯Java程式碼。
    • 使用java -classpath . JStackDemo執行應用程式。
  2. 獲取Java程序ID

    • 在命令列中使用jps命令檢視所有Java程序及其PID。
  3. 使用jstack獲取堆疊跟蹤

    • 假設你的Java應用程式的PID是1234,使用以下命令獲取執行緒堆疊跟蹤:
jstack 1234
     
  1. 分析輸出

    • jstack命令將輸出所有執行緒的堆疊跟蹤。你可以檢視每個執行緒的狀態和它們呼叫的方法。
  2. 查詢死鎖

    • 在輸出中,jstack會特別標記死鎖的執行緒,並顯示死鎖迴圈。例如:
    Found one Java-level deadlock:
    ===================
    "Thread-1":
        at JStackDemo$DeadlockDemo.run(JStackDemo.java:23)
        - waiting to lock monitor 0x00000007f7e8b8400 (object 0x00000007f7e8b8420, a java.lang.Class)
        - locked ownable synchronizer 0x00000007f7e8b8420 (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    "Thread-2":
        at JStackDemo$DeadlockDemo.run(JStackDemo.java:23)
        - waiting to lock monitor 0x00000007f7e8b8420 (object 0x00000007f7e8b8420, a java.lang.Class)
        - locked ownable synchronizer 0x00000007f7e8b8400 (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    Java stack information for the threads listed above:
    ===================================================
    "Thread-1":
            at JStackDemo$DeadlockDemo.run(JStackDemo.java:23)
            - waiting to lock <0x00000007f7e8b8400>
            - locked <0x00000007f7e8b8420>
    "Thread-2":
            at JStackDemo$DeadlockDemo.run(JStackDemo.java:23)
            - waiting to lock <0x00000007f7e8b8420>
            - locked <0x00000007f7e8b8400>
  1. 解決死鎖
    • 根據jstack的輸出,你可以分析死鎖的原因,並修改程式碼來避免死鎖,例如透過確保所有執行緒以相同的順序獲取鎖。

透過這個示例,你可以看到jstack是一個強大的工具,可以幫助你快速診斷執行緒問題和死鎖。

4. 使用jmap

jmap是一個命令列實用程式,用於生成Java堆轉儲快照或連線到正在執行的Java虛擬機器(JVM)並檢索有關堆的有用資訊。下面是一個簡單的Java應用程式示例,它將演示如何使用jmap來生成堆轉儲檔案。

示例Java應用程式程式碼

public class JmapDemo {
    private static final int LIST_SIZE = 10000;

    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();

        // 填充列表以使用大量記憶體
        for (int i = 0; i < LIST_SIZE; i++) {
            list.add(new byte[1024]); // 每個元素1KB
        }

        // 為了保持物件活躍,防止被GC回收
        keepReference(list);
    }

    private static void keepReference(List<Object> list) {
        // 此方法保持對list的引用,防止其被回收
        while (true) {
            try {
                // 讓執行緒休眠,模擬長時間執行的服務
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

使用jmap生成堆轉儲檔案

  1. 編譯並執行示例應用程式

    • 使用javac JmapDemo.java編譯Java程式碼。
    • 使用java -classpath . JmapDemo執行應用程式。
  2. 獲取Java程序ID

    • 在命令列中使用jps命令檢視所有Java程序及其PID。
  3. 使用jmap生成堆轉儲

    • 假設你的Java應用程式的PID是1234,使用以下命令生成堆轉儲檔案:
      jmap -dump:format=b,file=heapdump.hprof 1234
      
    • 這個命令會生成一個名為heapdump.hprof的堆轉儲檔案。
  4. 分析堆轉儲檔案

    • 使用MAT(Memory Analyzer Tool)或其他堆分析工具開啟heapdump.hprof檔案,分析記憶體使用情況和潛在的記憶體洩漏。
  5. 使用jmap列印堆資訊

    • 如果你只需要檢視堆的概覽資訊,可以使用:
      jmap -heap 1234
      
    • 這將列印出堆的詳細資訊,包括使用的記憶體、最大記憶體、GC策略等。
  6. 使用jmap列印類載入資訊

    • 要檢視類載入器的統計資訊,可以使用:
      jmap -clstats 1234
      
    • 這將列印出已載入的類的數量和相關資訊。
  7. 使用jmap列印 finalizer 佇列

    • 如果你懷疑有物件因為等待finalize()方法而被保留在記憶體中,可以使用:
      jmap -finalizerinfo 1234
      
    • 這將列印出等待finalize()方法的物件的資訊。

透過這個示例,你可以看到jmap是一個有用的工具,可以幫助你診斷記憶體相關問題,如記憶體洩漏和高記憶體使用。生成的堆轉儲檔案可以進一步使用其他分析工具進行深入分析。

5. 使用jstat

jstat是JDK提供的一個命令列工具,用於實時監控JVM的效能指標,如類載入、記憶體、垃圾收集等。下面是一個簡單的Java應用程式示例,它將演示如何使用jstat來監控JVM的執行情況。

示例Java應用程式程式碼

public class JstatDemo {
    private static final int ARRAY_SIZE = 1000000;
    private static final byte[] data = new byte[1024 * 1024]; // 1MB陣列

    public static void main(String[] args) {
        // 模擬記憶體分配
        for (int i = 0; i < ARRAY_SIZE; i++) {
            if (i % 100000 == 0) {
                // 模擬間歇性的記憶體分配
                data = new byte[1024 * 1024];
            }
        }

        // 模擬長時間執行的服務
        while (true) {
            try {
                Thread.sleep(1000); // 休眠1秒
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

使用jstat監控JVM效能指標

  1. 編譯並執行示例應用程式

    • 使用javac JstatDemo.java編譯Java程式碼。
    • 使用java -classpath . JstatDemo執行應用程式。
  2. 獲取Java程序ID

    • 在命令列中使用jps命令檢視所有Java程序及其PID。
  3. 使用jstat監控GC活動

    • 假設你的Java應用程式的PID是1234,使用以下命令監控GC活動:
      jstat -gc 1234
      
    • 這將顯示GC相關的統計資訊,如S0C、S1C、S0U、S1U(年輕代大小和使用情況)、EC、EU、OC、OU、MC、MU等。
  4. 監控類載入資訊

    • 使用以下命令監控類載入器的統計資訊:
      jstat -class 1234
      
    • 這將顯示已載入的類數量、已解除安裝的類數量等資訊。
  5. 監控編譯方法資訊

    • 使用以下命令監控JIT編譯器的統計資訊:
      jstat -compiler 1234
      
    • 這將顯示編譯任務的數量、編譯時間等資訊。
  6. 監控記憶體使用情況

    • 使用以下命令監控記憶體使用情況:
      jstat -gcutil 1234
      
    • 這將顯示堆記憶體的利用率,包括年輕代和老年代。
  7. 監控執行緒活動

    • 使用以下命令監控執行緒的統計資訊:
      jstat -thread 1234
      
    • 這將顯示執行緒總數、存活執行緒數、峰值執行緒數等資訊。
  8. 監控同步阻塞資訊

    • 使用以下命令監控同步阻塞資訊:
      jstat -sync 1234
      
    • 這將顯示同步操作的統計資訊,如監視器鎖的爭用情況。

透過這個示例,你可以看到jstat是一個實時監控工具,可以幫助你瞭解JVM的執行狀況,特別是在效能調優和故障排查時非常有用。透過監控不同的效能指標,你可以快速定位問題並採取相應的措施。

6. 使用jcmd

jcmd 是一個多功能的命令列工具,用於執行管理和診斷命令,獲取有關Java虛擬機器(JVM)和Java應用程式的資訊。下面是一個簡單的Java應用程式示例,它將演示如何使用 jcmd 來監控和管理JVM的執行情況。

示例Java應用程式程式碼

public class JcmdDemo {
    private static final int LIST_SIZE = 10000;

    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();

        // 填充列表以使用大量記憶體
        for (int i = 0; i < LIST_SIZE; i++) {
            list.add(new byte[1024]); // 每個元素1KB
        }

        // 模擬長時間執行的服務
        while (true) {
            try {
                Thread.sleep(1000); // 休眠1秒
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

使用jcmd監控和管理JVM

  1. 編譯並執行示例應用程式

    • 使用javac JcmdDemo.java編譯Java程式碼。
    • 使用java -classpath . JcmdDemo執行應用程式。
  2. 獲取Java程序ID

    • 在命令列中使用jps命令檢視所有Java程序及其PID。
  3. 使用jcmd獲取JVM資訊

    • 假設你的Java應用程式的PID是1234,使用以下命令獲取JVM的基本資訊:
      jcmd 1234 Help
      
    • 這將顯示所有可用的jcmd命令及其說明。
  4. 獲取執行緒堆疊跟蹤

    • 使用以下命令獲取所有執行緒的堆疊跟蹤:
      jcmd 1234 Thread.print
      
    • 這將輸出每個執行緒的呼叫棧。
  5. 監控GC活動

    • 使用以下命令監控GC活動:
      jcmd 1234 GC.class_histogram
      
    • 這將顯示所有載入的類的統計資訊。
  6. 生成堆轉儲檔案

    • 使用以下命令生成堆轉儲檔案:
      jcmd 1234 GC.heap_dump /path/to/heapdump.hprof
      
    • 這將生成一個名為heapdump.hprof的堆轉儲檔案,你可以使用MAT(Memory Analyzer Tool)或其他堆分析工具進行分析。
  7. 監控記憶體使用情況

    • 使用以下命令監控記憶體使用情況:
      jcmd 1234 GC.heap_info
      
    • 這將顯示堆記憶體的詳細資訊,包括年輕代和老年代的大小。
  8. 監控執行緒狀態

    • 使用以下命令監控執行緒狀態:
      jcmd 1234 Thread.print
      
    • 這將顯示所有執行緒的狀態和堆疊跟蹤。
  9. 監控編譯任務

    • 使用以下命令監控編譯任務:
      jcmd 1234 Compiler.code
      
    • 這將顯示JIT編譯器編譯的程式碼資訊。
  10. 監控類載入資訊

    • 使用以下命令監控類載入資訊:
      jcmd 1234 ClassLoader.stats
      
    • 這將顯示類載入器的統計資訊。

透過這個示例,你可以看到jcmd是一個強大的工具,可以執行多種管理和診斷命令。它不僅可以幫助你監控JVM的執行情況,還可以生成堆轉儲檔案進行深入分析。

7. 分析GC日誌

分析GC(垃圾收集)日誌是監控和最佳化Java應用程式效能的重要手段之一。GC日誌包含了JVM執行垃圾收集時的詳細資訊,比如收集前後的堆記憶體使用情況、收集所花費的時間等。下面是一個簡單的Java應用程式示例,它將演示如何產生GC日誌,並使用分析工具來解讀這些日誌。

示例Java應用程式程式碼

import java.util.ArrayList;
import java.util.List;

public class GcLogDemo {
    private static final int LIST_SIZE = 10000;

    public static void main(String[] args) {
        List<Byte[]> list = new ArrayList<>();

        // JVM引數設定,以產生GC日誌
        // -Xlog:gc*:file=gc.log 表示記錄所有GC相關日誌到gc.log檔案
        // -Xms100m -Xmx100m 設定JVM的初始堆大小和最大堆大小為100MB
        // JVM引數應放在java命令中,例如:
        // java -Xlog:gc*:file=gc.log -Xms100m -Xmx100m -classpath . GcLogDemo

        for (int i = 0; i < LIST_SIZE; i++) {
            // 分配記憶體,觸發GC
            list.add(new Byte[1024]);
        }

        // 讓GC有機會執行
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

使用分析工具解讀GC日誌

  1. 編譯並執行示例應用程式

    • 使用javac GcLogDemo.java編譯Java程式碼。
    • 執行應用程式時,確保包含了產生GC日誌的JVM引數,如上面註釋中所示。
  2. 產生GC日誌

    • 執行應用程式一段時間後,它將產生GC日誌到指定的檔案(例如gc.log)。
  3. 使用GC日誌分析工具

    • 可以使用多種工具來分析GC日誌,例如GCViewer、GCEasy、jClarity等。
    • 以GCViewer為例,你可以將GC日誌檔案拖放到GCViewer應用程式中,或者使用File -> Open來載入日誌檔案。
  4. 分析GC日誌內容

    • 在GCViewer中,你可以看到GC的概覽,包括GC的型別(Minor GC、Major GC、Full GC等)。
    • 觀察GC發生的時間點,以及每次GC所佔用的時間。
    • 分析堆記憶體的使用情況,包括Eden區、Survivor區、老年代等。
  5. 識別效能瓶頸

    • 如果發現GC時間過長或者頻繁發生,這可能是效能瓶頸的跡象。
    • 分析GC日誌可以幫助你確定是否需要調整JVM的記憶體設定或垃圾收集器策略。
  6. 調整JVM引數

    • 根據GC日誌的分析結果,你可能需要調整堆大小、Eden和Survivor區的比例、垃圾收集器型別等引數。
  7. 重新執行並監控

    • 在調整了JVM引數後,重新執行應用程式並監控GC日誌,以驗證效能是否有所改善。

透過這個示例,你可以看到如何透過產生和分析GC日誌來監控和最佳化Java應用程式的垃圾收集效能。這對於確保應用程式的穩定性和響應性至關重要。

8. 使用MAT(Memory Analyzer Tool)

MAT(Memory Analyzer Tool)是一個開源的Java堆分析器,它可以幫助我們發現記憶體洩漏和最佳化記憶體使用。下面是一個簡單的Java應用程式示例,它將產生一個堆轉儲檔案,然後我們可以使用MAT來分析這個檔案。

示例Java應用程式程式碼

import java.util.ArrayList;
import java.util.List;

public class MatDemo {
    private static List<Object> leakedObjects = new ArrayList<>();

    public static void main(String[] args) {
        // 模擬記憶體洩漏:不斷建立新物件,並保留對它們的引用
        for (int i = 0; i < 10000; i++) {
            leakedObjects.add(new byte[1024]); // 每個元素1KB
        }

        // 觸發堆轉儲,可以透過-XX:+HeapDumpOnOutOfMemoryError引數自動觸發
        // 或者透過程式呼叫System.gc()來建議JVM進行垃圾收集
        // 然後使用jmap工具手動觸發堆轉儲
        try {
            System.out.println("Initiating heap dump - please wait...");
            // 假設jmap工具已經生成了堆轉儲檔案 matdemo.hprof
            // 如果需要在程式中觸發,可以使用Runtime.getRuntime().gc();
            // 然後呼叫Thread.sleep(5000); 讓GC有足夠的時間執行
            // 接著使用jmap生成堆轉儲:jmap -dump:format=b,file=matdemo.hprof <pid>
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 程式將保持執行,以等待MAT分析
        while (true) {
            try {
                Thread.sleep(60000); // 休眠60秒
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

使用MAT分析堆轉儲檔案

  1. 編譯並執行示例應用程式

    • 使用javac MatDemo.java編譯Java程式碼。
    • 執行應用程式,確保透過JVM引數或jmap工具生成了堆轉儲檔案,例如matdemo.hprof
  2. 啟動MAT

    • 下載並啟動MAT工具。
  3. 載入堆轉儲檔案

    • 在MAT中,選擇"File" -> "Open Heap Dump",然後選擇之前生成的matdemo.hprof檔案。
  4. 分析記憶體使用情況

    • MAT將分析堆轉儲檔案,並展示概覽資訊,包括記憶體使用概覽、類例項、GC roots等。
  5. 查詢記憶體洩漏

    • 使用MAT的"Analyzer" -> "Run"功能,MAT將分析可能的記憶體洩漏。
    • 檢查"Leak Suspects Report",它將列出可能的記憶體洩漏物件。
  6. 檢視物件的引用情況

    • 在"Dominator Tree"檢視中,可以檢視哪些物件佔用了最多的記憶體。
    • 在"Reference Chain"檢視中,可以檢視物件被引用的路徑。
  7. 分析特定的物件

    • 如果你懷疑某個物件存在記憶體洩漏,可以在"Classes"檢視中找到這個類,然後雙擊例項檢視詳細資訊。
  8. 使用OQL查詢

    • MAT支援物件查詢語言(OQL),你可以使用OQL來查詢特定的物件集合或模式。
  9. 匯出和儲存分析結果

    • 你可以將分析結果匯出為報告,以供進一步分析或記錄。

透過這個示例,你可以看到MAT是一個功能強大的工具,可以幫助你分析Java堆轉儲檔案,發現記憶體洩漏和最佳化記憶體使用。MAT提供了豐富的檢視和查詢功能,使得分析過程更加高效和深入。

9. 使用Profilers

Profilers 是一類用於效能分析的工具,它們可以幫助開發者識別應用程式中的效能瓶頸。下面是一個簡單的Java應用程式示例,它將演示如何使用 Profilers 工具(如JProfiler或YourKit Java Profiler)來監控和分析應用程式的效能。

示例Java應用程式程式碼

public class ProfilerDemo {
    private static final int NUM_ITERATIONS = 1000000;

    public static void main(String[] args) {
        // 執行一些計算密集型的任務
        long result = computeSum(0, NUM_ITERATIONS);

        // 模擬長時間執行的服務
        while (true) {
            try {
                Thread.sleep(1000); // 休眠1秒
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    private static long computeSum(long start, long end) {
        long sum = 0;
        for (long i = start; i < end; i++) {
            sum += i;
        }
        return sum;
    }
}

使用Profilers工具監控和分析效能

  1. 編譯並執行示例應用程式

    • 使用javac ProfilerDemo.java編譯Java程式碼。
    • 執行應用程式時,確保啟動了Profilers工具,並將應用程式附加到Profilers中。
  2. 附加Profilers到應用程式

    • 開啟JProfiler或YourKit Java Profiler等Profilers工具。
    • 在Profilers中選擇“附加到應用程式”,並選擇正在執行的ProfilerDemo程序。
  3. 監控CPU使用情況

    • 在Profilers的CPU Profiling檢視中,監控應用程式的CPU使用情況。
    • 識別佔用CPU時間最多的方法,這可能是效能瓶頸。
  4. 分析記憶體使用

    • 使用記憶體分析功能來監控應用程式的記憶體使用情況。
    • 檢視記憶體分配情況,識別記憶體洩漏或高記憶體消耗的類。
  5. 識別執行緒活動和鎖爭用

    • 監控執行緒活動,檢視執行緒的狀態和鎖的使用情況。
    • 識別死鎖或執行緒爭用,這可能影響應用程式的響應時間。
  6. 執行取樣分析

    • 使用Profilers的取樣分析功能來收集一段時間內的呼叫資料。
    • 分析取樣結果,找出熱點方法和呼叫路徑。
  7. 使用呼叫樹檢視

    • 檢視呼叫樹檢視,瞭解方法呼叫的層次結構和時間消耗。
  8. 分析方法執行情況

    • 識別執行時間最長的方法,並檢視它們的呼叫者和被呼叫者。
  9. 最佳化程式碼

    • 根據分析結果,最佳化程式碼以提高效能,例如透過減少不必要的計算、改進資料結構或演算法。
  10. 重新分析最佳化後的程式碼

    • 在最佳化程式碼後,重新執行Profilers分析,驗證效能改進。

透過這個示例,你可以看到Profilers工具如何幫助開發者監控和分析Java應用程式的效能。透過識別效能瓶頸和記憶體問題,開發者可以採取相應的最佳化措施來提高應用程式的效率和響應速度。

10. 最後

在實際工作中,我們還需要監控系統資源,比如監控CPU、記憶體、磁碟I/O和網路等系統資源的使用情況,以確定是否是系統資源限制導致的問題。平時也可以閱讀和理解JVM規範,V 哥推薦一本 JAVA程式設計師人手一本的書《JAVA虛擬機器規範》,強烈建議好好讀一下哦。如果本文內容對你有幫助,麻煩一鍵三連加關注,程式設計師路上,我們一起攙扶前行。

相關文章