目錄
什麼是效能監控?
效能監控是持續收集、分析和追蹤系統資源使用狀況的過程,目的是:
- 及早發現效能瓶頸
- 優化資源使用率
- 確保系統穩定運行
- 為容量規劃提供數據
監控的四個層級
應用層(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 詳情
關鍵要點:
- Context Switch - 過高表示執行緒競爭激烈
- RSS vs VSZ - 實體記憶體 vs 虛擬記憶體
- IOWait - 高則表示磁碟瓶頸
- Swap - 避免使用,會嚴重影響效能
- GC - Full GC 頻繁表示記憶體問題
- 執行緒數 - 過多會增加上下文切換
- 檔案描述符 - 注意限制和洩漏
監控原則:
- 建立基線(Baseline)- 了解正常狀態
- 持續監控 - 使用 Prometheus + Grafana
- 告警設定 - 及時發現問題
- 日誌保留 - 便於問題回溯
建立日期:2025-11-11