在 Java 應用開發與調優過程中,直觀掌握 JVM 堆記憶體使用情況至關重要。雖然專業工具(VisualVM、JConsole、Arthas)功能強大,但有時我們需要一個輕量、可嵌入、可客製化的監控視窗——例如整合在桌面應用的「狀態欄」中,或作為教學演示工具。本文將完整解析一個基於 Runtime.getRuntime() 與 Swing JProgressBar 的記憶體監視器實作,涵蓋核心 API 解讀、多執行緒架構設計與 UI 響應優化。
java.lang.Runtime 提供了三個關鍵方法,構成監控的數據源:
long totalMemory():JVM 當前向作業系統申請的總堆大小(會隨 -Xms/-Xmx 動態擴縮,單位:位元組)。long freeMemory():當前堆中未被佔用的空閒空間(包含新生代、老年代未使用區域,單位:位元組)。long maxMemory():JVM 堆可達到的最大上限(對應 -Xmx 參數,單位:位元組)。usedMemory = totalMemory() - freeMemory() 才是「真實佔用量」。totalMemory() 會隨 GC 後釋放空間而縮小(若未達 -Xmx),並不等於最大堆大小。監控進度條建議以 maxMemory() 為分母,或以 totalMemory() 為分母顯示「當前堆使用率」,兩者語義不同需明確標示。
監控屬於典型的「後台輪詢數據 → 前台更新 UI」場景,必須遵守 **Swing 單執行緒規則**:所有 UI 操作必須在 EDT (Event Dispatch Thread) 執行,耗時運算(含 Thread.sleep)嚴禁在 EDT 執行。
| 類別 | 角色 | 核心職責 |
|---|---|---|
JMemoryDemo |
啟動器 | 建立主視窗、置中顯示、啟動 EDT |
MainFrame |
視圖層 | 構建 UI(JProgressBar 垂直顯示、JLabel 顯示數值)、啟動監控執行緒 |
ProgressThread |
模型/控制器 | 後台輪詢 Runtime API、計算使用率、透過 SwingUtilities.invokeLater 更新 UI |
public void run() {
// 1. 初始化進度條屬性 (需在 EDT 設定,這裡假設構造時已設定)
// pbar.setMinimum(0); pbar.setMaximum(100); pbar.setStringPainted(true);
while (!isInterrupted()) { // 支援優雅停止
try {
// 2. 獲取記憶體快照 (極快,非阻塞)
Runtime rt = Runtime.getRuntime();
long total = rt.totalMemory(); // 當前堆大小
long free = rt.freeMemory(); // 當前空閒
long used = total - free; // 實際使用
long max = rt.maxMemory(); // 最大堆
// 3. 計算顯示數值 (單位轉 KB,計算百分比)
int totalKB = (int)(total / 1024);
int freeKB = (int)(free / 1024);
int usedPercent = (total > 0) ? (int)((used * 100.0) / max) : 0; // 建議以 max 為分母
// 4. 【核心】回 EDT 更新 UI (不可直接在這裡 setValue/setText!)
final String memText = String.format("Free: %,d KB | Total: %,d KB | Max: %,d KB",
freeKB, totalKB, max/1024);
final int percent = usedPercent;
SwingUtilities.invokeLater(() -> {
if (pbar.isDisplayable()) { // 防止視窗關閉後仍更新
pbar.setValue(percent);
pbar.setString("Heap Usage: " + percent + "%");
label.setText(memText);
}
});
// 5. 休眠 1 秒 (在背景執行緒睡眠,不阻塞 UI)
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 保持中斷狀態
break; // 優雅退出迴圈
} catch (Exception e) {
e.printStackTrace(); // 記錄錯誤但不讓監控掛掉
}
}
}
public class MainFrame extends JFrame {
private ProgressThread monitorThread; // 持有引用以便關閉時停止
private void jbInit() {
// ... UI 初始化代碼 ...
jProgressBar1.setOrientation(JProgressBar.VERTICAL); // 垂直進度條更直觀
jProgressBar1.setStringPainted(true);
// 啟動監控執行緒
monitorThread = new ProgressThread(jProgressBar1, jLabel2);
monitorThread.start();
}
// 覆寫視窗關閉事件,確保執行緒優雅停止
@Override
protected void processWindowEvent(WindowEvent e) {
super.processWindowEvent(e);
if (e.getID() == WindowEvent.WINDOW_CLOSING) {
if (monitorThread != null) monitorThread.interrupt(); // 發送中斷信號
System.exit(0);
}
}
}
錯誤: pbar.setValue(...) 寫在 run() 迴圈裡。
後果: UI 凍結、繪製異常、數據競爭、隨機崩潰。
鐵律: **所有 Swing setter 必須包裹在 SwingUtilities.invokeLater(() -> { ... }) 中。**
InterruptedException 導致無法關閉錯誤: catch (InterruptedException e) { } 吞掉異常繼續跑。
對策: Thread.currentThread().interrupt(); break; 重設標誌並跳出迴圈。
對於週期性 UI 更新任務,javax.swing.Timer 更簡潔且自動在 EDT 執行:
// 在 MainFrame 構造器中
Timer timer = new Timer(1000, e -> {
Runtime rt = Runtime.getRuntime();
long used = rt.totalMemory() - rt.freeMemory();
int percent = (int)((used * 100.0) / rt.maxMemory());
jProgressBar1.setValue(percent);
jProgressBar1.setString("Heap: " + percent + "%");
jLabel2.setText(String.format("Used: %,d MB / Max: %,d MB", used/1024/1024, rt.maxMemory()/1024/1024));
});
timer.start(); // 自動在 EDT 執行,視窗關閉需 timer.stop()
若需監控新生代、老年代、Metaspace 獨立使用率,請使用 ManagementFactory.getMemoryPoolMXBeans(),功能遠超 Runtime。
這個簡易監視器雖小,却完整展示了 Java 監控體系的核心知識點:Runtime API 的正確解讀、Swing 執行緒模型的嚴格遵守、背景輪詢與 UI 解耦的標準模式。掌握這些,您不僅能自製監控工具,更能深入理解 VisualVM 等專業工具的底層運作原理。對於生產環境嚴肅監控,請務必採用 JMX Exporter + Prometheus + Grafana 標準棧。