CodeGym /課程 /JAVA 25 SELF /程式碼效能剖析與最佳化:工具與方法

程式碼效能剖析與最佳化:工具與方法

JAVA 25 SELF
等級 63 , 課堂 4
開放

1. 效能剖析導論

效能剖析就像為你的程式做健康檢查:我們不只是看「體溫」(監控),而是要找出應用程式「哪裡痛」、哪些地方很慢、在哪裡耗費了過多的記憶體或資源。

效能剖析是蒐集並分析程式執行情形的過程,目的是找出瓶頸(bottlenecks)與低效的程式區段。不同於監控通常只追蹤整體指標(CPU 載入、記憶體、執行緒數量),剖析能往內部查看:了解哪些方法被呼叫最頻繁、它們花了多少時間、建立了多少物件,以及記憶體外洩究竟發生在何處。

什麼時候真的需要做效能剖析?

  • 應用程式「很卡」,但不清楚原因。
  • 記憶體用量突然上升。
  • 更新程式碼後,某些操作變慢了。
  • 需要了解為何伺服器資源不足。

順帶一提,幾乎每位開發者一生中都至少優化錯過地方一次。為什麼?因為靠感覺幾乎無法準確找出瓶頸——這正是剖析器存在的價值。

效能剖析的關鍵指標

  • 方法執行時間(CPU 剖析):哪些方法耗時最多?程式在哪裡「吃」CPU?
  • 記憶體使用(Memory 剖析):哪些物件最常被建立?哪些物件在記憶體中停留過久?
  • 物件數量:我們是否建立了過多同類型物件?
  • 執行緒:執行緒是否過多?是否存在鎖定或競爭(deadlock, contention)?
  • 方法呼叫:呼叫堆疊的深度如何?是否出現無出口的遞迴?

2. 效能剖析工具

在 Java 世界中,有多個經典(而且免費!)的工具可用於效能剖析。下面介紹幾個主要的。

VisualVM

VisualVM 是一款免費工具,隨 JDK(自 JDK 6 起)提供。它可以:

  • 連線到本機與遠端 JVM。
  • 檢視記憶體、執行緒、CPU、垃圾回收。
  • 製作 heap dump 並加以分析。
  • 對應用進行 CPU 與記憶體剖析。

如何啟動 VisualVM?
它通常位於 JDK 目錄:<path_to_JDK>/bin/jvisualvm
啟動後,選擇 Java 應用的行程,就能觀察它的生命週期,彷彿在看水族箱裡的魚(只不過這裡的「魚」是物件與執行緒)。

JProfiler, YourKit

這些是商業但非常強大的工具。它們可以:

  • 剖析記憶體、CPU、執行緒。
  • 分析記憶體「快照」(heap dump)。
  • 尋找洩漏、長時間鎖定、慢方法。
  • 與 IDE 與 CI/CD 整合。

起步用 VisualVM 已足夠;若你成長到更大型的專案,值得考慮這些工具。

Java Flight Recorder (JFR)

JFR 是內建於 JDK 的 JVM 事件蒐集工具。它非常輕量,幾乎不影響效能,能蒐集以下資訊:

  • 方法執行時間。
  • 垃圾回收。
  • 執行緒、鎖定、錯誤。

JFR 非常適合在生產環境使用,當你不能讓應用變慢時尤為好用。

3. 實作:剖析一個簡單應用

我們來建立一個迷你計算器,能執行長時間計算並保存操作歷史(這樣會有迴圈、集合與記憶體操作可觀察)。

程式範例:「慢速計算器」

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

public class SlowCalculator {
    private final List<String> history = new ArrayList<>();

    public int add(int a, int b) {
        simulateHeavyOperation();
        int result = a + b;
        history.add(a + " + " + b + " = " + result);
        return result;
    }

    public int multiply(int a, int b) {
        simulateHeavyOperation();
        int result = a * b;
        history.add(a + " * " + b + " = " + result);
        return result;
    }

    public List<String> getHistory() {
        return history;
    }

    // 模擬 "繁重" 的操作
    private void simulateHeavyOperation() {
        for (int i = 0; i < 5_000_000; i++) {
            Math.sqrt(i);
        }
    }
}

現在來看主類別:

public class Main {
    public static void main(String[] args) {
        SlowCalculator calc = new SlowCalculator();
        for (int i = 0; i < 10; i++) {
            calc.add(i, i * 2);
            calc.multiply(i, i + 5);
        }
        System.out.println("操作歷史:");
        for (String entry : calc.getHistory()) {
            System.out.println(entry);
        }
    }
}

如何剖析這個應用?

  1. 編譯並執行應用。
  2. 打開 VisualVMjvisualvm)。
  3. 找到你的行程(通常可依類別名稱 Main)。
  4. 切換到 CPU Profiler 分頁並按 Start。
  5. 讓程式運行一段時間(或再次觸發耗時的操作)。
  6. 查看哪些方法耗時最多。

問題:你認為哪個方法會最「重」?
答案:當然是 simulateHeavyOperation()——因為它跑了 5_000_000 次的巨大迴圈並呼叫 Math.sqrt

4. 常見的效能問題

演算法過慢

最常見的原因:選錯演算法或資料結構。例如,用 list 搜尋而不是 HashMap,或用冒泡排序而不是快速排序。

範例:

// 慢速搜尋
for (String s : list) {
    if (s.equals("target")) {
        // 找到
    }
}

較好的作法是使用 SetMap 來加速搜尋。

記憶體洩漏

記憶體洩漏是指物件仍然「存活」(仍被參考),即使它已不再需要。這會導致記憶體用量攀升,最終引發 OutOfMemoryError

public class MemoryLeakDemo {
    private static List<byte[]> leakyList = new ArrayList<>();

    public static void main(String[] args) {
        while (true) {
            leakyList.add(new byte[1_000_000]); // 1 MB
            try { Thread.sleep(100); } catch (InterruptedException ignored) {}
        }
    }
}

如何找出洩漏?
VisualVM 產生 heap dump,看看哪些物件佔用最多記憶體,以及為何仍被參考。

過度建立物件

如果你在迴圈中建立大量同類型物件,不僅會加重垃圾回收器負擔,還可能讓應用變慢。

for (int i = 0; i < 1_000_000; i++) {
    String s = new String("hello"); // 不佳!
}

較好的作法是使用常量或字串池(String pool)。

執行緒鎖定

若多個執行緒競爭同一資源(例如同步化的方法),就可能導致鎖定並使效能下降。

public synchronized void doWork() {
    // ...
}

如何找出?
VisualVM 的 Threads 分頁可以看到哪些執行緒在「卡住」,以及原因。

5. 最佳化方法

先量測,再最佳化

最佳化的首要準則:不要最佳化那些不會造成瓶頸的部分。

先做剖析,找出「熱點」,再修改程式。有時最「明顯」的區塊只佔了 1% 的時間,而真正的「怪獸」藏在函式庫或某個意想不到的地方。

用剖析器尋找熱點

熱點是指在應用程式執行中占用最大時間比例的方法或程式區段。

VisualVM 的 CPU Profiler 分頁可以看到:

  • 依執行時間排序方法。
  • 查看 stack trace:誰呼叫了誰。
  • 記住,有時「罪魁禍首」不是你的程式,而是函式庫甚至是 JDK 本身。

最佳化範例

範例 1:替換演算法
如果你發現大多時間花在 list 搜尋,將 List 換成 HashSet

Set<String> set = new HashSet<>(list);
if (set.contains("target")) {
    // 快!
}

範例 2:減少配置次數
不要在迴圈中不斷建立新物件,改用重複利用或 StringBuilder

// 不佳:
for (int i = 0; i < 10000; i++) {
    String s = "結果: " + i;
}

// 較佳:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.setLength(0);
    sb.append("結果: ").append(i);
    String s = sb.toString();
}

範例 3:快取
如果你發現某個耗時的方法常以相同參數被呼叫,請使用快取。

Map<Integer, Double> sqrtCache = new HashMap<>();
public double cachedSqrt(int x) {
    return sqrtCache.computeIfAbsent(x, Math::sqrt);
}

6. 示範:讓我們的計算器加速

問題:simulateHeavyOperation() 非常耗時

步驟 1:剖析
VisualVM 可看到幾乎所有時間都花在 Math.sqrt(i) 上,位於 5_000_000 次的迴圈中。

步驟 2:最佳化
如果這只是負載模擬——移除它或減少迭代次數。
如果這是實際商業邏輯——請思考是否可以:

  • 快取結果。
  • 使用更快的演算法。
  • 將計算移到獨立執行緒(若對使用者不關鍵)。

最佳化示例:

private void simulateHeavyOperation() {
    // 原本 5_000_000,改為 100_000
    for (int i = 0; i < 100_000; i++) {
        Math.sqrt(i);
    }
}

步驟 3:驗證結果
再次啟動剖析——程式運行更快,CPU 負載下降。

7. 視覺化:最佳化流程

flowchart TD
    A[啟動應用程式]
    B["效能剖析(VisualVM)"]
    C[找出瓶頸]
    D[程式碼最佳化]
    E[再次剖析]
    F[效能改善]

    A --> B --> C --> D --> E --> F
    E --> C

8. 剖析與最佳化的常見錯誤

錯誤 1:憑感覺就開始最佳化。 開發者常在未量測問題所在就開始改碼。結果是投入很多工,成效卻很有限。

錯誤 2:在「不真實」環境中剖析。剖析必須在接近真實流量與資料的條件下進行。空載或不具代表性的測試可能看不出真正問題。

錯誤 3:忽視記憶體洩漏。若不看 heap dump 並分析參考關係,程式可能「越來越胖」而不自知,最後直接當掉。

錯誤 4:追逐微不足道的最佳化。別把數天花在只佔 0.1% 執行時間的程式碼上。先解決主要瓶頸。

錯誤 5:忽視執行緒與同步。在多執行緒應用中,效能問題常不是演算法,而是鎖定與等待(synchronized、contention)。

錯誤 6:修改後忘了再做剖析。最佳化之後務必重新驗證結果:有時所謂的「最佳化」反而會變慢!

1
問卷/小測驗
日誌紀錄,等級 63,課堂 4
未開放
日誌紀錄
日誌紀錄、監控與效能剖析
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION