作業系統效能監控指標完全指南

系統層級和 Java 應用程式的效能指標詳解,涵蓋 CPU、記憶體、I/O、網路等關鍵監控點。


目錄

  1. 什麼是效能監控?
  2. CPU 相關指標
  3. 記憶體相關指標
  4. I/O 相關指標
  5. 行程(Process)相關指標
  6. 網路相關指標
  7. Java 特定指標
  8. 監控工具使用
  9. 最佳實踐
  10. 效能問題診斷
  11. 總結

什麼是效能監控?

效能監控是持續收集、分析和追蹤系統資源使用狀況的過程,目的是:

  • 及早發現效能瓶頸
  • 優化資源使用率
  • 確保系統穩定運行
  • 為容量規劃提供數據

監控的四個層級

應用層(Application)
  ↓ API 回應時間、錯誤率、吞吐量
JVM 層(Runtime)
  ↓ Heap 使用、GC 頻率、執行緒狀態
作業系統層(OS)
  ↓ CPU、記憶體、I/O、網路
硬體層(Hardware)
  ↓ 實體資源使用狀況

核心監控指標

類別 關鍵指標 監控目的
CPU 使用率、上下文切換、Load Average 計算資源是否充足
記憶體 使用率、Swap、Page Fault 記憶體是否足夠
I/O IOPS、吞吐量、等待時間 磁碟是否成為瓶頸
網路 頻寬使用、封包遺失、連線數 網路是否正常
應用 回應時間、QPS、錯誤率 服務品質是否達標

CPU 相關指標

1. Context Switch(上下文切換)

定義:CPU 從一個行程/執行緒切換到另一個行程/執行緒的過程。

為什麼重要

  • 過多的上下文切換會導致 CPU 時間浪費在切換上,而非實際工作
  • 每次切換需要保存/恢復暫存器狀態、記憶體映射等

正常值與異常值

正常:< 1000-5000 次/秒(取決於系統負載)
警告:5000-10000 次/秒
危險:> 10000 次/秒(可能有問題)

造成高上下文切換的原因

  • 執行緒數量過多
  • 鎖競爭激烈(多執行緒頻繁搶鎖)
  • 頻繁的 I/O 操作
  • 過度使用 sleep()wait()

Java 範例問題

// ❌ 不好的做法 - 造成大量上下文切換
ExecutorService executor = Executors.newFixedThreadPool(1000); // 過多執行緒
for (int i = 0; i < 10000; i++) {
    executor.submit(() -> {
        // 短時間的小任務
        System.out.println("Task");
    });
}

// ✅ 好的做法 - 合理的執行緒數
int cpuCores = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(cpuCores * 2);

監控命令

# 查看上下文切換次數
vmstat 1

# 輸出範例:
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
#  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
#  2  0      0 180000  50000 300000    0    0     0     5  500 8000  5  2 93  0  0
#                                                              ↑
#                                                          上下文切換次數

# 詳細查看每個行程的上下文切換
pidstat -w 1

# 輸出範例:
# 12:00:01  PID   cswch/s nvcswch/s  Command
# 12:00:02 1234    100.00    500.00  java
#                  ↑         ↑
#              自願切換   非自願切換

自願切換 vs 非自願切換

  • 自願切換(voluntary context switches): 行程主動放棄 CPU(如等待 I/O)
  • 非自願切換(non-voluntary context switches): 行程被強制切換(時間片用完、高優先級行程搶佔)

2. Load Average(負載平均值)

定義:系統在過去 1、5、15 分鐘內的平均負載。

如何解讀

$ uptime
15:30:01 up 10 days, 2:15, 1 user, load average: 2.50, 1.80, 1.20
                                                    ↑     ↑     ↑
                                                  1分鐘  5分鐘 15分鐘

判斷標準(假設 4 核 CPU):

Load Average = 1.0  → 25% 使用率(1/4 核心忙碌)
Load Average = 2.0  → 50% 使用率(2/4 核心忙碌)
Load Average = 4.0  → 100% 使用率(4/4 核心滿載)
Load Average > 4.0  → 超載(有行程在等待 CPU)

計算公式

理想 Load Average ≈ CPU 核心數 × 0.7

例如:4 核 CPU
- 理想負載:2.8 左右
- 警告閾值:3.2(80%)
- 危險閾值:4.0(100%)

趨勢分析

1.50, 1.80, 2.10  → 負載持續上升(需關注)
2.80, 2.00, 1.50  → 負載下降(恢復正常)
0.50, 0.60, 0.55  → 負載穩定且低(系統健康)
8.00, 7.50, 3.00  → 最近有突發高負載

3. CPU Usage(CPU 使用率)

指標分類

us (user)    - 使用者空間 CPU 時間(應用程式)
sy (system)  - 核心空間 CPU 時間(系統呼叫)
id (idle)    - 閒置時間
wa (iowait)  - 等待 I/O 的時間
st (steal)   - 虛擬化環境被偷走的時間

監控命令

# 即時監控 CPU 使用率
top

# 每秒更新一次
vmstat 1

# 各個 CPU 核心的使用率
mpstat -P ALL 1

# 輸出範例:
# 12:00:01  CPU    %usr   %sys  %iowait    %idle
# 12:00:02  all   45.00  10.00     5.00    40.00
# 12:00:02    0   50.00  12.00     3.00    35.00
# 12:00:02    1   40.00   8.00     7.00    45.00

異常情況判斷

高 User CPU (us > 80%)

  • 應用程式計算密集
  • 可能有死迴圈或無限迴圈
  • Java:頻繁的 GC、大量物件創建

高 System CPU (sy > 30%)

  • 系統呼叫過多
  • 頻繁的上下文切換
  • 大量的 I/O 操作

高 IOWait (wa > 20%)

  • 磁碟 I/O 瓶頸
  • 資料庫查詢慢
  • 檔案系統效能問題

Java 範例問題

// ❌ 造成高 CPU 使用率
// 1. 死迴圈
while (true) {
    // 沒有 sleep 或 break 條件
}

// 2. 忙等待(Busy Waiting)
while (!condition) {
    // 持續檢查但不釋放 CPU
}

// ✅ 正確做法
while (!condition) {
    Thread.sleep(100); // 釋放 CPU
}

// 或使用 wait/notify 機制
synchronized (lock) {
    while (!condition) {
        lock.wait(); // 正確的等待方式
    }
}

4. CPU Affinity(CPU 親和性)

定義:將行程/執行緒綁定到特定的 CPU 核心。

優點

  • 減少 CPU 快取失效(Cache Miss)
  • 提高 L1/L2 快取命中率
  • NUMA 架構下更重要

設定方式

# 查看行程的 CPU 親和性
taskset -p <PID>

# 將行程綁定到 CPU 0-3
taskset -cp 0-3 <PID>

# 啟動時指定 CPU 親和性
taskset -c 0-3 java -jar app.jar

Java JVM 參數

# 綁定到特定 CPU(Linux)
taskset -c 0-3 java -XX:+UseParallelGC -jar app.jar

記憶體相關指標

1. RSS (Resident Set Size)

定義:行程實際佔用的實體記憶體大小(不包含 swap)。

包含內容

  • Heap(堆積)記憶體
  • Stack(堆疊)記憶體
  • Code(程式碼)段
  • 共享函式庫的常駐部分

監控命令

# 查看行程記憶體使用
ps aux | grep java

# 輸出範例:
# USER   PID  %CPU %MEM    VSZ   RSS TTY   STAT START   TIME COMMAND
# user  1234  15.0 10.5 4000000 850000 ?   Sl   10:00  12:34 java -jar app.jar
#                        ↑       ↑
#                       VSZ     RSS (KB)

# 更詳細的記憶體資訊
cat /proc/<PID>/status | grep -E 'VmRSS|VmSize|VmSwap'

# 輸出範例:
# VmSize:  4000000 kB  (虛擬記憶體)
# VmRSS:    850000 kB  (實體記憶體)
# VmSwap:    50000 kB  (Swap)

Java 應用的 RSS 組成

RSS = Java Heap + Metaspace + Thread Stacks + Direct Memory + Native Memory

典型 Java 應用範例:
- Heap:          512 MB  (設定 -Xmx512m)
- Metaspace:     100 MB  (類別元資料)
- Thread Stack:   50 MB  (100 執行緒 × 512 KB)
- Direct Memory:  64 MB  (NIO Buffer)
- Native Memory:  50 MB  (JNI、C++ 函式庫)
- Code Cache:     50 MB  (JIT 編譯後的程式碼)
───────────────────────
Total RSS:      ~826 MB

RSS 過高的原因

  • Heap 設定過大(-Xmx)
  • 執行緒數量過多
  • Direct Memory 洩漏(NIO Buffer 未釋放)
  • Native Memory 洩漏(JNI)
  • Memory-mapped Files

2. VSZ (Virtual Memory Size)

定義:行程的虛擬記憶體總大小,包含所有映射的記憶體。

包含內容

  • RSS(實體記憶體)
  • Swap(交換記憶體)
  • Memory-mapped Files
  • 共享函式庫的全部大小
  • 預留但未使用的記憶體

VSZ vs RSS

VSZ(虛擬記憶體)≥ RSS(實體記憶體)

範例:
VSZ = 4 GB    → 行程"宣稱"要用的記憶體
RSS = 850 MB  → 行程實際使用的記憶體

為什麼 VSZ 很大但 RSS 很小?

  • Java 預先保留(reserve)記憶體,但尚未提交(commit)
  • Memory-mapped Files(如 JAR 檔案)
  • 共享函式庫的虛擬記憶體映射

Java 範例

# Java 應用啟動時
java -Xms128m -Xmx2g -jar app.jar

# 初始狀態:
# VSZ: 2.5 GB  (預留 2GB heap + Metaspace + Stack...)
# RSS: 200 MB  (實際只用了 128MB heap + 其他)

# 運行一段時間後:
# VSZ: 2.5 GB  (不變)
# RSS: 1.2 GB  (heap 成長到 1GB + 其他)

3. Heap Memory(堆積記憶體)

定義:Java 物件分配的主要記憶體區域。

結構

Java Heap
├── Young Generation (年輕代)
│   ├── Eden Space (伊甸區)
│   └── Survivor Spaces (倖存者區)
│       ├── S0
│       └── S1
└── Old Generation (老年代)
    └── Tenured Space

JVM 參數設定

# 初始堆積大小
-Xms512m

# 最大堆積大小
-Xmx2g

# 年輕代大小
-Xmn512m

# Eden 與 Survivor 比例(預設 8:1:1)
-XX:SurvivorRatio=8

# 完整範例
java -Xms1g -Xmx4g -Xmn1g -XX:SurvivorRatio=8 -jar app.jar

監控 Heap 使用

# 使用 jstat 監控 GC 和 Heap
jstat -gc <PID> 1000

# 輸出範例:
# S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU
# 10240  10240  0      8192   819200   716800   1048576    524288   51200  48000
#   ↑      ↑     ↑      ↑       ↑        ↑         ↑          ↑        ↑      ↑
#  S0容量 S1容量 S0使用 S1使用 Eden容量 Eden使用 Old容量  Old使用  Meta容量 Meta使用

# 查看 Heap 詳細資訊
jmap -heap <PID>

記憶體洩漏檢測

# 生成 Heap Dump
jmap -dump:format=b,file=heap.bin <PID>

# 使用 MAT (Eclipse Memory Analyzer) 分析
# 或使用 jhat
jhat heap.bin
# 訪問 http://localhost:7000

4. Metaspace(元空間)

定義:Java 8+ 用來儲存類別元資料的記憶體區域(取代 PermGen)。

內容

  • 類別的結構資訊
  • 方法元資料
  • 常數池
  • 註解資訊

JVM 參數

# 初始 Metaspace 大小
-XX:MetaspaceSize=128m

# 最大 Metaspace 大小(預設無限制)
-XX:MaxMetaspaceSize=256m

# 範例
java -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -jar app.jar

Metaspace 溢出

java.lang.OutOfMemoryError: Metaspace

常見原因:
1. 動態產生大量類別(CGLib、動態代理)
2. ClassLoader 洩漏(重新部署時未卸載舊類別)
3. 過多的 Lambda 表達式
4. 反射使用過多

監控 Metaspace

# 使用 jstat
jstat -gc <PID> 1000

# MC (Metaspace Capacity)
# MU (Metaspace Used)

# 詳細資訊
jcmd <PID> GC.heap_info

5. Stack Memory(堆疊記憶體)

定義:每個執行緒的私有記憶體,用於存放區域變數、方法呼叫資訊。

每個執行緒的預設 Stack 大小

Linux x64:   1 MB
Windows:     1 MB
macOS:       512 KB

JVM 參數

# 設定每個執行緒的 Stack 大小
-Xss512k    # 512 KB
-Xss1m      # 1 MB

# 範例:100 執行緒 × 512 KB = 50 MB Stack 記憶體
java -Xss512k -jar app.jar

Stack 記憶體計算

總 Stack 記憶體 = 執行緒數量 × 每個執行緒的 Stack 大小

範例:
- 執行緒數:200
- Stack 大小:1 MB/執行緒
- 總 Stack 記憶體:200 MB

StackOverflowError

// 遞迴過深
public void recursion(int depth) {
    if (depth > 0) {
        recursion(depth - 1); // StackOverflowError
    }
}

// 解決方式:
// 1. 增加 Stack 大小:-Xss2m
// 2. 改用迴圈
// 3. 使用尾遞迴優化

6. Direct Memory(直接記憶體)

定義:NIO 使用的堆外記憶體(Off-Heap Memory)。

用途

  • ByteBuffer.allocateDirect()
  • MappedByteBuffer(記憶體映射檔案)
  • Netty 的 Direct Buffer

JVM 參數

# 設定最大 Direct Memory(預設 = -Xmx)
-XX:MaxDirectMemorySize=512m

# 範例
java -Xmx2g -XX:MaxDirectMemorySize=1g -jar app.jar

監控 Direct Memory

# 使用 JMX 監控
jconsole <PID>
# 查看 java.nio.BufferPool

# 或使用 Java 程式碼
import java.lang.management.BufferPoolMXBean;
import java.lang.management.ManagementFactory;

List<BufferPoolMXBean> pools = ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class);
for (BufferPoolMXBean pool : pools) {
    System.out.println("Name: " + pool.getName());
    System.out.println("Count: " + pool.getCount());
    System.out.println("Memory Used: " + pool.getMemoryUsed());
    System.out.println("Total Capacity: " + pool.getTotalCapacity());
}

Direct Memory 洩漏

// ❌ 不好的做法 - 未釋放 Direct Buffer
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024); // 1 MB
// 使用 buffer...
// 忘記清理,導致 Direct Memory 洩漏

// ✅ 好的做法
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
try {
    // 使用 buffer
} finally {
    // Direct Buffer 會在 GC 時自動清理,但也可以手動清理
    if (buffer instanceof sun.nio.ch.DirectBuffer) {
        ((sun.nio.ch.DirectBuffer) buffer).cleaner().clean();
    }
}

7. Swap Memory(交換記憶體)

定義:當實體記憶體不足時,作業系統將不常用的記憶體頁面寫入磁碟。

為什麼 Swap 是效能殺手?

記憶體存取速度:~100 ns
磁碟存取速度:  ~10 ms

磁碟比記憶體慢 100,000 倍!

監控 Swap

# 查看系統 Swap 使用情況
free -h

# 輸出範例:
#               total        used        free      shared  buff/cache   available
# Mem:           15Gi       8.0Gi       2.0Gi       500Mi       5.0Gi       6.5Gi
# Swap:          8.0Gi       1.5Gi       6.5Gi
#                            ↑
#                       已使用 Swap

# 查看哪些行程使用 Swap
for pid in $(ps -ef | grep java | awk '{print $2}'); do
    swap=$(cat /proc/$pid/status 2>/dev/null | grep VmSwap | awk '{print $2}')
    if [ "$swap" != "0" ]; then
        echo "PID $pid: Swap = $swap kB"
    fi
done

# 查看 Swap In/Out 頻率
vmstat 1

# 輸出範例:
# procs -----------memory---------- ---swap-- -----io----
#  r  b   swpd   free   buff  cache   si   so    bi    bo
#  2  0 100000 200000 50000 300000    0    0     5    10
#                                     ↑    ↑
#                                 Swap In  Swap Out

避免 Swap 的策略

1. 禁用 Swap(生產環境 Java 應用)

# 臨時禁用
sudo swapoff -a

# 永久禁用(編輯 /etc/fstab,註解掉 swap 行)
# /dev/sda2  none  swap  sw  0  0  ← 註解掉

2. 調整 Swappiness

# 查看當前值(預設 60)
cat /proc/sys/vm/swappiness

# 設定為 10(降低 Swap 傾向)
sudo sysctl vm.swappiness=10

# 永久設定(編輯 /etc/sysctl.conf)
vm.swappiness=10

3. 鎖定記憶體(Java)

# 使用 mlockall 鎖定記憶體(防止被 Swap)
java -XX:+AlwaysPreTouch -jar app.jar

8. Page Fault(分頁錯誤)

定義:當程式存取的記憶體頁面不在實體記憶體中時發生。

分類

  • Minor Page Fault: 頁面在記憶體中,但未映射(快速)
  • Major Page Fault: 頁面需從磁碟讀取(慢速)

監控

# 查看行程的 Page Fault
ps -o min_flt,maj_flt,cmd -p <PID>

# 輸出範例:
# MINFL  MAJFL CMD
# 50000   1000 java -jar app.jar
#   ↑      ↑
# Minor  Major

# 使用 time 命令查看
/usr/bin/time -v java -jar app.jar

# 輸出包含:
# Minor (reclaiming a frame) page faults: 50000
# Major (requiring I/O) page faults: 1000

高 Major Page Fault 的影響

  • 大量磁碟 I/O
  • 應用程式變慢
  • 可能是記憶體不足的徵兆

I/O 相關指標

1. Disk I/O(磁碟 I/O)

指標說明

r/s    - 每秒讀取次數
w/s    - 每秒寫入次數
rkB/s  - 每秒讀取 KB 數
wkB/s  - 每秒寫入 KB 數
await  - 平均 I/O 等待時間(毫秒)
%util  - 磁碟使用率

監控命令

# iostat - 磁碟 I/O 統計
iostat -x 1

# 輸出範例:
# Device   r/s   w/s   rkB/s   wkB/s  await  %util
# sda     10.0  20.0   160.0   320.0   5.5   45.0
#         ↑     ↑      ↑       ↑       ↑      ↑
#      讀次數 寫次數 讀速度  寫速度  等待時間 使用率

# iotop - 查看哪些行程在使用 I/O
sudo iotop -o

# 輸出範例:
# TID  PRIO  USER     DISK READ  DISK WRITE  COMMAND
# 1234  be/4  user      10.5 M/s    5.2 M/s  java -jar app.jar

判斷標準

await(等待時間):
- < 10 ms     : 優秀
- 10-20 ms    : 正常
- 20-50 ms    : 需關注
- > 50 ms     : 有瓶頸

%util(使用率):
- < 70%       : 健康
- 70-85%      : 繁忙
- 85-95%      : 壓力大
- > 95%       : 接近飽和

Java 應用的磁碟 I/O

// 常見造成大量磁碟 I/O 的操作:

// 1. 頻繁的檔案寫入
FileWriter writer = new FileWriter("log.txt", true);
for (int i = 0; i < 100000; i++) {
    writer.write("Log entry\n"); // 每次都觸發 I/O
    writer.flush();              // 強制寫入磁碟
}

// ✅ 改善:使用緩衝
BufferedWriter writer = new BufferedWriter(new FileWriter("log.txt", true));
for (int i = 0; i < 100000; i++) {
    writer.write("Log entry\n"); // 先寫入緩衝區
}
writer.close(); // 批次寫入磁碟

// 2. 大量的資料庫操作
// ❌ 逐筆插入
for (int i = 0; i < 10000; i++) {
    statement.execute("INSERT INTO users VALUES (...)");
}

// ✅ 批次操作
PreparedStatement ps = conn.prepareStatement("INSERT INTO users VALUES (?, ?)");
for (int i = 0; i < 10000; i++) {
    ps.setInt(1, i);
    ps.setString(2, "name");
    ps.addBatch();
}
ps.executeBatch();

2. Network I/O(網路 I/O)

監控命令

# netstat - 網路連線統計
netstat -s

# 輸出範例:
# Tcp:
#     1000000 segments received
#     950000 segments sent out
#     500 segments retransmitted    ← 重傳(網路問題)
#     100 bad segments received

# sar - 網路流量統計
sar -n DEV 1

# 輸出範例:
# IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s
# eth0    5000.00   4500.00   6400.00   5800.00
#         ↑         ↑          ↑         ↑
#      收包數    送包數     接收速率   傳送速率

# 查看網路連線狀態
ss -s

# 輸出範例:
# Total: 150
# TCP:   80 (estab 60, closed 10, listen 5)
# UDP:   20

Java 網路 I/O 監控

# 查看 Java 應用的網路連線
lsof -i -a -p <PID>

# 或使用 ss
ss -antp | grep java

# 輸出範例:
# ESTAB  0  0  192.168.1.100:8080  192.168.1.200:54321  users:(("java",pid=1234))
# ESTAB  0  0  192.168.1.100:3306  192.168.1.150:3306   users:(("java",pid=1234))

常見網路問題

1. Too many open files

# 查看行程打開的檔案數(包括 socket)
lsof -p <PID> | wc -l

# 查看檔案描述符限制
ulimit -n

# 增加限制
ulimit -n 65535

# 永久設定(編輯 /etc/security/limits.conf)
* soft nofile 65535
* hard nofile 65535

2. TIME_WAIT 過多

# 查看 TIME_WAIT 連線數
ss -ant | grep TIME_WAIT | wc -l

# 調整核心參數(/etc/sysctl.conf)
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30

3. 連線洩漏

// ❌ 未關閉連線
Socket socket = new Socket("localhost", 8080);
// 使用 socket...
// 忘記關閉,導致連線洩漏

// ✅ 使用 try-with-resources
try (Socket socket = new Socket("localhost", 8080)) {
    // 使用 socket
} // 自動關閉

行程(Process)相關指標

1. 行程狀態

狀態代碼(Linux):

R (Running)       - 正在執行或可執行(在執行佇列中)
S (Sleeping)      - 可中斷的睡眠(等待事件)
D (Disk Sleep)    - 不可中斷的睡眠(通常是等待 I/O)
Z (Zombie)        - 殭屍行程(已終止但未被回收)
T (Stopped)       - 已停止(收到 SIGSTOP)

監控命令

# 查看行程狀態
ps aux | grep java

# 輸出範例:
# USER   PID  %CPU %MEM  VSZ    RSS  TTY  STAT START  TIME COMMAND
# user  1234  15.0 10.5 4000000 850000 ?  Sl   10:00  12:34 java -jar app.jar
#                                           ↑
#                                        狀態代碼

# STAT 欄位說明:
# S    - Sleeping
# l    - 多執行緒(has threads)
# +    - 前景行程群組
# <    - 高優先級
# N    - 低優先級

# 查看詳細狀態
cat /proc/<PID>/status | grep State

# 輸出:
# State: S (sleeping)

異常狀態診斷

D 狀態(不可中斷睡眠)

# 如果行程長時間處於 D 狀態,可能是:
# 1. 磁碟 I/O 阻塞
# 2. NFS 掛載點無回應
# 3. 核心錯誤

# 診斷方法:
# 查看行程的 stack trace
cat /proc/<PID>/stack

# 查看系統 I/O 狀況
iostat -x 1

Z 狀態(殭屍行程)

# 殭屍行程的形成:
# 子行程已終止,但父行程未呼叫 wait() 回收

# 查看殭屍行程
ps aux | grep Z

# 清理方式:
# 1. 找到父行程
ps -o ppid= -p <ZOMBIE_PID>

# 2. 重啟父行程(如果可以)
# 3. 或發送 SIGCHLD 給父行程
kill -s SIGCHLD <PARENT_PID>

2. 執行緒數量

監控命令

# 查看行程的執行緒數
ps -o nlwp <PID>

# 或使用 top
top -H -p <PID>

# 或直接查看
cat /proc/<PID>/status | grep Threads

# 輸出:
# Threads: 150

Java 執行緒監控

# 使用 jstack 查看執行緒堆疊
jstack <PID>

# 使用 jcmd
jcmd <PID> Thread.print

# 統計執行緒狀態
jstack <PID> | grep 'java.lang.Thread.State' | sort | uniq -c

# 輸出範例:
#  50 java.lang.Thread.State: RUNNABLE
#  80 java.lang.Thread.State: WAITING
#  20 java.lang.Thread.State: TIMED_WAITING

執行緒數量過多的問題

// 問題原因:
// 1. 執行緒池未限制大小
ExecutorService executor = Executors.newCachedThreadPool(); // 無限制

// 2. 執行緒洩漏(未關閉)
for (int i = 0; i < 1000; i++) {
    new Thread(() -> {
        // 長時間執行的任務
    }).start();
}

// ✅ 解決方式:
// 1. 使用固定大小的執行緒池
int threads = Runtime.getRuntime().availableProcessors() * 2;
ExecutorService executor = Executors.newFixedThreadPool(threads);

// 2. 設定執行緒池拒絕策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10, 50, 60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000),
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒絕策略
);

// 3. 監控執行緒池
((ThreadPoolExecutor) executor).getActiveCount();
((ThreadPoolExecutor) executor).getPoolSize();

3. Open Files(打開的檔案)

定義:行程打開的檔案描述符數量(包括檔案、socket、pipe)。

監控命令

# 查看行程打開的檔案數
lsof -p <PID> | wc -l

# 查看限制
ulimit -n

# 查看系統限制
cat /proc/sys/fs/file-max

# 查看系統當前使用的檔案描述符
cat /proc/sys/fs/file-nr

# 輸出:
# 12000  0  1048576
#   ↑    ↑    ↑
# 已分配 已使用 最大值

分類查看

# 查看打開的檔案類型
lsof -p <PID> | awk '{print $5}' | sort | uniq -c

# 輸出範例:
#  100 REG   (一般檔案)
#  500 IPv4  (IPv4 socket)
#   50 IPv6  (IPv6 socket)
#   10 pipe  (管道)
#    5 DIR   (目錄)

# 查看哪些檔案被打開
lsof -p <PID> | grep REG

# 查看網路連線
lsof -i -a -p <PID>

Too many open files 錯誤

java.io.IOException: Too many open files

// 常見原因:
// 1. 檔案未關閉
FileInputStream fis = new FileInputStream("file.txt");
// 使用 fis...
// 忘記關閉

// 2. Socket 未關閉
Socket socket = new Socket("localhost", 8080);
// 使用 socket...
// 忘記關閉

// ✅ 解決方式:
// 1. 使用 try-with-resources
try (FileInputStream fis = new FileInputStream("file.txt")) {
    // 使用 fis
} // 自動關閉

// 2. 增加檔案描述符限制
ulimit -n 65535

// 3. 監控打開的檔案數
Runtime.getRuntime().exec("lsof -p " + ProcessHandle.current().pid());

網路相關指標

1. TCP 連線狀態

TCP 狀態機

LISTEN       - 監聽中(伺服器等待連線)
SYN_SENT     - 已發送連線請求(客戶端)
SYN_RECEIVED - 已收到連線請求(伺服器)
ESTABLISHED  - 連線已建立(資料傳輸中)
FIN_WAIT_1   - 主動關閉,等待確認
FIN_WAIT_2   - 主動關閉,等待對方關閉
CLOSE_WAIT   - 被動關閉,等待應用關閉
LAST_ACK     - 被動關閉,等待最後確認
TIME_WAIT    - 主動關閉,等待 2MSL
CLOSED       - 連線關閉

監控命令

# 統計各狀態的連線數
ss -ant | awk '{print $1}' | sort | uniq -c

# 或使用 netstat
netstat -ant | awk '{print $6}' | sort | uniq -c

# 輸出範例:
#    5 LISTEN
#  100 ESTABLISHED
#   50 TIME_WAIT
#   10 CLOSE_WAIT

常見問題

TIME_WAIT 過多

# 原因:主動關閉的連線需等待 2MSL(通常 60 秒)

# 影響:
# - 佔用大量 port
# - 可能導致 "Cannot assign requested address"

# 解決方式:
# 1. 調整核心參數
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.tcp_fin_timeout=30

# 2. 使用連線池
# 3. 改為長連線(Keep-Alive)

CLOSE_WAIT 過多

# 原因:應用程式未正確關閉 socket

# 診斷:
lsof -i -a -p <PID> | grep CLOSE_WAIT

# 解決方式:
# 檢查程式碼,確保 socket 正確關閉

2. Packet Loss(封包遺失)

監控命令

# 查看網路介面統計
ip -s link

# 輸出範例:
# 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP>
#     RX: bytes  packets  errors  dropped
#        1000000  100000      10       5
#                                ↑       ↑
#                            接收錯誤  丟棄

# 查看詳細錯誤統計
netstat -i

# 輸出範例:
# Iface  MTU  RX-OK RX-ERR RX-DRP  TX-OK TX-ERR TX-DRP
# eth0  1500 100000     10      5  95000      0      0

# 使用 sar 監控
sar -n EDEV 1

# 輸出範例:
# IFACE   rxerr/s  txerr/s  rxdrop/s  txdrop/s
# eth0       0.10     0.00      0.50      0.00

封包遺失的影響

  • TCP 重傳增加
  • 延遲增加
  • 吞吐量下降

Java 特定指標

1. Garbage Collection(垃圾回收)

GC 類型

Minor GC (Young GC) - 清理年輕代
Major GC (Old GC)   - 清理老年代
Full GC             - 清理整個 Heap

監控命令

# 使用 jstat 監控 GC
jstat -gc <PID> 1000

# 輸出範例:
#  S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    YGC  YGCT   FGC  FGCT
# 10240  10240    0    8192   819200   716800   1048576    524288   51200  48000   50  0.500    2  0.100
#                                                                                     ↑    ↑      ↑    ↑
#                                                                             Young GC次數 時間 Full GC次數 時間

# 詳細 GC 日誌
java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -jar app.jar

# GC 日誌範例:
# 2024-01-15T10:00:01.234+0800: [GC (Allocation Failure)
#   [PSYoungGen: 512000K->8192K(614400K)] 512000K->8192K(2016256K), 0.0123456 secs]

GC 調優參數

# 選擇 GC 演算法
-XX:+UseSerialGC         # Serial GC(單執行緒)
-XX:+UseParallelGC       # Parallel GC(多執行緒,吞吐量優先)
-XX:+UseG1GC             # G1 GC(低延遲)
-XX:+UseZGC              # ZGC(超低延遲,JDK 11+)

# G1 GC 調優
-XX:MaxGCPauseMillis=200       # 最大暫停時間目標
-XX:G1HeapRegionSize=16m       # Region 大小
-XX:InitiatingHeapOccupancyPercent=45  # 觸發 Mixed GC 的閾值

# 完整範例
java -Xms4g -Xmx4g \
     -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=200 \
     -XX:+PrintGCDetails \
     -XX:+PrintGCDateStamps \
     -Xloggc:gc.log \
     -jar app.jar

GC 問題診斷

Full GC 頻繁

原因:
1. 老年代空間不足
2. Metaspace 不足
3. 記憶體洩漏
4. 大物件直接進入老年代

診斷:
# 查看 Heap 使用情況
jmap -heap <PID>

# 生成 Heap Dump 分析
jmap -dump:live,format=b,file=heap.bin <PID>

# 使用 MAT 分析記憶體洩漏

GC 暫停時間過長

原因:
1. Heap 太大
2. 老年代物件太多
3. 使用了不適合的 GC 演算法

解決方式:
1. 使用 G1 或 ZGC
2. 調整 Heap 大小
3. 優化物件生命週期

2. JIT Compilation(即時編譯)

定義:JVM 將熱點程式碼(Hot Code)編譯成機器碼以提升效能。

監控命令

# 查看 JIT 編譯統計
jstat -compiler <PID>

# 輸出範例:
# Compiled Failed Invalid   Time   FailedType FailedMethod
#     5000      0       0  120.00          0

# 查看 CodeCache 使用情況
jstat -printcompilation <PID> 1000

# 查看詳細編譯日誌
java -XX:+PrintCompilation -jar app.jar

JIT 參數

# 設定 CodeCache 大小
-XX:ReservedCodeCacheSize=256m

# 分層編譯(預設開啟)
-XX:+TieredCompilation

# 調整編譯閾值
-XX:CompileThreshold=10000

3. Class Loading(類別載入)

監控命令

# 查看類別載入統計
jstat -class <PID> 1000

# 輸出範例:
# Loaded  Bytes  Unloaded  Bytes     Time
#  10000 20000000      500 1000000   15.00
#    ↑      ↑          ↑      ↑       ↑
# 已載入 載入大小  已卸載 卸載大小  載入時間

# 查看詳細載入資訊
java -XX:+TraceClassLoading -XX:+TraceClassUnloading -jar app.jar

監控工具使用

1. top / htop

基本使用

# top - 系統整體監控
top

# 按鍵操作:
# M - 依記憶體排序
# P - 依 CPU 排序
# 1 - 顯示各個 CPU 核心
# H - 顯示執行緒
# k - 終止行程

# htop(需安裝)- 更友善的介面
htop

# 功能:
# - 彩色顯示
# - 滑鼠支援
# - 樹狀顯示
# - 更直觀的操作

2. vmstat(虛擬記憶體統計)

# 每秒更新一次
vmstat 1

# 輸出範例:
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
#  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
#  2  0      0 180000 50000 300000    0    0     5    10  500 8000  5  2 93  0  0
#  ↑  ↑      ↑    ↑      ↑     ↑      ↑    ↑     ↑     ↑    ↑    ↑   ↑  ↑  ↑  ↑  ↑

# 欄位說明:
# r  - 執行佇列長度(waiting for CPU)
# b  - 不可中斷睡眠的行程數
# swpd - 已使用的 Swap
# free - 空閒記憶體
# buff - Buffer 快取
# cache - Page 快取
# si - Swap In(從磁碟讀入記憶體)
# so - Swap Out(從記憶體寫入磁碟)
# bi - Block In(讀取 block/s)
# bo - Block Out(寫入 block/s)
# in - 每秒中斷次數
# cs - 每秒上下文切換次數
# us - User CPU 時間
# sy - System CPU 時間
# id - Idle CPU 時間
# wa - I/O Wait 時間
# st - Steal 時間(虛擬化)

3. iostat(I/O 統計)

# 監控磁碟 I/O
iostat -x 1

# 輸出範例:
# Device  r/s   w/s   rkB/s   wkB/s  await  %util
# sda    10.0  20.0   160.0   320.0   5.5   45.0
#        ↑     ↑      ↑       ↑       ↑      ↑
#      讀/s  寫/s  讀速度  寫速度  等待時間 使用率

# 欄位說明:
# r/s    - 每秒讀取次數
# w/s    - 每秒寫入次數
# rkB/s  - 每秒讀取 KB
# wkB/s  - 每秒寫入 KB
# await  - 平均 I/O 等待時間(毫秒)
# %util  - 磁碟使用率(接近 100% 表示飽和)

4. pidstat(行程統計)

# CPU 使用率
pidstat -p <PID> 1

# 記憶體使用
pidstat -r -p <PID> 1

# I/O 統計
pidstat -d -p <PID> 1

# 上下文切換
pidstat -w -p <PID> 1

# 執行緒統計
pidstat -t -p <PID> 1

5. Java 專用工具

# jps - 列出 Java 行程
jps -lv

# jstat - JVM 統計
jstat -gc <PID> 1000        # GC 統計
jstat -gcutil <PID> 1000    # GC 使用率
jstat -gccause <PID> 1000   # GC 原因

# jstack - 執行緒堆疊
jstack <PID> > thread_dump.txt

# jmap - 記憶體映射
jmap -heap <PID>            # Heap 資訊
jmap -histo <PID>           # 物件統計
jmap -dump:live,format=b,file=heap.bin <PID>  # Heap Dump

# jcmd - 多功能命令
jcmd <PID> VM.uptime        # JVM 運行時間
jcmd <PID> GC.heap_info     # Heap 資訊
jcmd <PID> Thread.print     # 執行緒堆疊
jcmd <PID> VM.system_properties  # 系統屬性

效能問題診斷

問題 1:CPU 使用率過高

診斷步驟

# 1. 找出高 CPU 的執行緒
top -H -p <PID>

# 記下高 CPU 的執行緒 TID,例如:1234

# 2. 將 TID 轉為 16 進位
printf "%x\n" 1234
# 輸出:4d2

# 3. 在 jstack 中查找對應執行緒
jstack <PID> | grep -A 20 "0x4d2"

# 4. 分析執行緒在做什麼

常見原因

  • 死迴圈
  • 正規表達式效能問題
  • 大量字串拼接
  • 頻繁的反射呼叫
  • GC 過於頻繁

問題 2:記憶體使用過高

診斷步驟

# 1. 查看 Heap 使用情況
jmap -heap <PID>

# 2. 查看物件統計(找出大量物件)
jmap -histo:live <PID> | head -20

# 3. 生成 Heap Dump
jmap -dump:live,format=b,file=heap.bin <PID>

# 4. 使用 MAT 分析
# 下載 Eclipse Memory Analyzer Tool
# 打開 heap.bin 檔案
# 查看 Leak Suspects(洩漏疑點)

常見原因

  • 記憶體洩漏(物件無法被 GC)
  • 快取過大
  • 靜態集合持有大量物件
  • ThreadLocal 未清理
  • 連線未關閉

問題 3:應用程式變慢

診斷步驟

# 1. 檢查系統負載
uptime
top

# 2. 檢查 I/O 等待
iostat -x 1

# 3. 檢查網路
netstat -s | grep retrans

# 4. 檢查 GC
jstat -gcutil <PID> 1000

# 5. 檢查執行緒狀態
jstack <PID> | grep 'java.lang.Thread.State' | sort | uniq -c

常見原因

  • 資料庫查詢慢
  • 網路延遲
  • 磁碟 I/O 慢
  • 鎖競爭
  • GC 頻繁

問題 4:執行緒死鎖

診斷步驟

# 1. 使用 jstack 檢測死鎖
jstack <PID> | grep -A 10 "Found one Java-level deadlock"

# 2. 分析死鎖資訊
# jstack 會自動檢測並報告死鎖

# 輸出範例:
# Found one Java-level deadlock:
# =============================
# "Thread-1":
#   waiting to lock monitor 0x00007f8a9c004e18 (object 0x000000076ab0f7a0, a java.lang.String),
#   which is held by "Thread-2"
# "Thread-2":
#   waiting to lock monitor 0x00007f8a9c002ac8 (object 0x000000076ab0f7d0, a java.lang.String),
#   which is held by "Thread-1"

預防死鎖

// 使用固定順序獲取鎖
synchronized (lock1) {
    synchronized (lock2) {
        // 操作
    }
}

// 使用 tryLock 帶超時
Lock lock1 = new ReentrantLock();
if (lock1.tryLock(1, TimeUnit.SECONDS)) {
    try {
        // 操作
    } finally {
        lock1.unlock();
    }
}

效能調優建議

1. JVM 參數調優

# 生產環境推薦配置(8GB 記憶體機器)
java -server \
     -Xms4g -Xmx4g \
     -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m \
     -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=200 \
     -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/var/log/java/heap_dump.hprof \
     -XX:+PrintGCDetails \
     -XX:+PrintGCDateStamps \
     -Xloggc:/var/log/java/gc.log \
     -XX:+UseGCLogFileRotation \
     -XX:NumberOfGCLogFiles=5 \
     -XX:GCLogFileSize=20M \
     -Dfile.encoding=UTF-8 \
     -Duser.timezone=Asia/Taipei \
     -jar app.jar

2. 系統參數調優

# /etc/sysctl.conf

# 網路調優
net.core.somaxconn = 1024
net.ipv4.tcp_max_syn_backlog = 2048
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30

# 檔案描述符
fs.file-max = 1000000

# 記憶體
vm.swappiness = 10
vm.overcommit_memory = 1

# 套用設定
sudo sysctl -p
# /etc/security/limits.conf

# 增加檔案描述符限制
* soft nofile 65535
* hard nofile 65535
* soft nproc 4096
* hard nproc 4096

3. 監控告警設定

關鍵指標閾值

CPU 使用率:> 80% 警告,> 90% 嚴重
記憶體使用率:> 85% 警告,> 95% 嚴重
磁碟使用率:> 80% 警告,> 90% 嚴重
Load Average:> CPU 核心數 × 0.7 警告
GC 暫停時間:> 1 秒警告
Full GC 頻率:> 1 次/小時警告
執行緒數:> 500 警告
檔案描述符:> 10000 警告

總結

快速檢查清單

系統層級

# 1. 整體健康檢查
uptime               # Load Average
free -h              # 記憶體使用
df -h                # 磁碟使用
iostat -x 1 3        # I/O 狀況

# 2. CPU 檢查
top                  # CPU 使用率
vmstat 1 5           # 上下文切換
mpstat -P ALL 1 3    # 各核心使用率

# 3. 記憶體檢查
free -h              # 記憶體使用
vmstat -s            # 記憶體統計
cat /proc/meminfo    # 詳細資訊

# 4. I/O 檢查
iostat -x 1 5        # 磁碟 I/O
iotop -o             # I/O 使用行程

Java 應用層級

# 1. 基本檢查
jps -lv              # Java 行程
jcmd <PID> VM.uptime # 運行時間

# 2. 記憶體檢查
jmap -heap <PID>     # Heap 資訊
jstat -gc <PID> 1000 # GC 統計

# 3. 執行緒檢查
jstack <PID>         # 執行緒堆疊
top -H -p <PID>      # 執行緒 CPU

# 4. 效能檢查
jstat -gcutil <PID>  # GC 使用率
jcmd <PID> GC.heap_info  # Heap 詳情

關鍵要點

  1. Context Switch - 過高表示執行緒競爭激烈
  2. RSS vs VSZ - 實體記憶體 vs 虛擬記憶體
  3. IOWait - 高則表示磁碟瓶頸
  4. Swap - 避免使用,會嚴重影響效能
  5. GC - Full GC 頻繁表示記憶體問題
  6. 執行緒數 - 過多會增加上下文切換
  7. 檔案描述符 - 注意限制和洩漏

監控原則

  • 建立基線(Baseline)- 了解正常狀態
  • 持續監控 - 使用 Prometheus + Grafana
  • 告警設定 - 及時發現問題
  • 日誌保留 - 便於問題回溯

建立日期:2025-11-11

🔗相關文章