Java 程序員的 Linux 性能調優寶典:三大經典場景深度剖析
本文整理了Linux系統性能問題排查的通用方法論和實踐,將針對以下三個經典場景展開探討:
- I/O性能瓶頸
- CPU飆升
- 偶發CPU飆升
同時考慮到筆者文章的受眾面大部分都是Java開發人員,所以復現問題故障的例子也都采用Java進行編碼部署復現,對應的示例也都會在案例排查的最后展開說明。

一、應用程序延遲升高
第一個案例是用戶反饋系統延遲升高,網卡打開緩慢。從開發者的角度一定要明白,所有表現為卡頓、延遲的原因很大概率是系統資源吃緊,只有在資源分配不足的情況下,才會導致程序運行阻塞,無法及時處理用戶的請求。
關于服務器的閾值指標,按照業界通用的經驗,對應CPU和內存的負載要求的合理上限為:
- CPU使用率控制在75%左右
- 內存使用率控制在80%以內
- 虛擬內存盡可能保持在0%
- 負載不超過CPU核心數的75%
筆者一般會先通過top查看操作系統的CPU利用率,這里筆者因為是個人服務器原因則采用更強大、更直觀的htop查看個人服務器的資源使用情況,對應的安裝指令如下:
sudo apt update
sudo apt install htop而本次htop輸出的指標如下:
- 服務器為6核,對應的CPU使用率分別是3.9、0、0、2.7、0.7、1.4,按照業界的通用標準,當前服務器各核心CPU使用率較低,但需結合系統負載綜合判斷
- Mem代表了內存使用率,內存一般情況下是用于存儲程序代碼和數據,分為物理內存和虛擬內存,物理內存顯示內存接近8G僅用了1G不到,使用率不到80%,說明資源冗余
- Swp顯示交換空間即虛擬內存的使用情況,可以看到也僅僅用了32M,并沒有大量的內存數據被置換到交換空間,結合第2點來看,內存資源充足
- Tasks顯示進程數和線程數一共有35個進程,這35個進程對應100個線程處理,Kthr顯示指標為0說明有0個內核線程,而Running為1說明有一個用戶進程在運行
- 而系統平均負載近1分鐘為4.96,按照業界標準CPU核心數*0.75作為系統負載的運算閾值,如果超過這個值則說明系統處于高負載狀態,很明顯我們的6核服務器系統負載偏高了
綜合來看,服務器系統負載偏高但各CPU核心使用率較低,結合內存使用情況,問題可能出現在I/O資源等待上,此時我們就要從I/O資源角度進一步排查問題:

我們從I/O資源排查入手,通過vmstat 1執行每秒一次的監控指標輸出,以筆者的服務器為例,可以看到如下幾個指標:
- r:按照文檔解釋為The number of runnable processes (running or waiting for run time)即正在運行或等待運行的進程數,如果大于CPU核心數則說明CPU處于過載狀態,而當前服務器這個值為0,說明隊列處理狀態良好
- b::按照文檔解釋為The number of processes blocked waiting for I/O to complete即等待I/O完成的進程數,從參數b可以看出有大量進程等待I/O,說明當前服務器存在I/O瓶頸。
- swpd:the amount of swap memory used即交換空間也就是虛擬內存的使用,而當前服務器已被使用30468說明存在緩存置換,由此參數結合buff(緩存中尚未寫入磁盤的內容)和cache(從磁盤加載出來的緩存數據)來看,當前內存資源持續升高,存在讀寫虛擬內存的情況,存在I/O性能瓶頸。
- 從bo來看有大量任務進行每秒寫塊
- 針對CPU一個板塊輸出的us(用戶代碼執行時間)、sy(內核執行時間)、id(空閑時間)、wa(等待I/O的時間),其中wa即等待I/O的時間持續處于一個高數值的狀態,更進一步明確CPU在空轉,等待I/O完成,而I/O資源處于吃緊的狀態

考慮為I/O資源瓶頸,我們優先從網絡I/O角度排查問題,這里筆者采用nload進行網絡資源診斷,如果沒有下載的可以自行通過yum或者apt的方式自行下載,這里筆者也貼出ubuntu服務器的下載指令:
# ubuntu下載安裝nload
sudo apt install nload -y鍵入nload實時輸出網絡帶寬的使用情況,可以看到:
- 輸入流量(incoming)即下載流量,當前網速基本控制在1KB,僅在最大網速的20%左右,一般認為只有當前網速無限接近于最大網速才可認為帶寬使用率接近飽和
- 輸出流量(outgoing)即上傳流量,同理當前也僅僅使用8%,也未達到飽和的閾值
所以I/O資源吃緊的問題原因并非網絡I/O,我們需要進一步從服務器磁盤I/O角度進一步排查:

所以從這些指標來看,存在大量的線程在等待I/O資源的分配而進入阻塞,所以筆者基于iostat -x 1使每一秒都輸出更詳細的信息,可以看到sdd盤對應的磁盤忙碌百分比util基本跑滿100%,已基本接近飽和,此時基本是確認有大量線程在進行I/O讀寫任務了,且查看I/O讀寫指標:
- 每秒讀寫的吞吐量w/s為175
- 每秒寫入wkB/s的172704KB
- SSD盤util即I/O資源利用率為100%,已經遠超業界閾值60%,說明存在I/O性能瓶頸,需要補充說明的是當CPU100%時進程調度會因為操作系統優先級設置的原因并不會導致進程阻塞,但是I/O設備則不同,它不能區分優先級進行I/O中斷響應,所以這個數值高的情況下就會使得大量I/O請求阻塞
- 寫請求的平均等待時間w_await為5151ms
換算下來172704KB/175每秒寫入的速率為987KB每秒,由此可確定因為磁盤性能讀寫性能瓶頸導致大量I/O讀寫任務阻塞,進而導致服務器卡頓,用戶體驗差:

所以,對于系統延遲嚴重的情況,整體排查思路為:
- 通過top指令查看CPU使用率,若正常進入步驟2
- 基于vmstat查看內存使用率和I/O資源情況
- 基于nload查看網絡I/O使用情況
- 基于iostat查看網絡I/O和磁盤I/O情況最終確認問題
本例子的最后筆者也給出本次故障問題的示例代碼:
/**
* 啟動磁盤I/O操作以模擬高I/O負載
* 通過創建多個I/O任務線程來模擬高磁盤I/O負載
*/
private static void startDiskIOOperations() {
log.info("開始高I/O磁盤操作...");
log.info("在另一個終端中運行 'iostat -x 1' 來監控磁盤利用率。");
// 創建固定線程數的線程池
executor = Executors.newFixedThreadPool(NUM_THREADS);
// 提交多個任務以連續寫入磁盤
for (int i = 0; i < NUM_THREADS; i++) {
executor.submit(new IOTask(i));
}
log.info("磁盤I/O操作已啟動,使用 {} 個線程", NUM_THREADS);
}
/**
* 執行連續寫入操作以模擬高I/O的任務
* 該類負責執行磁盤I/O操作,通過不斷寫入和清空文件來模擬高I/O負載
*/
static class IOTask implements Runnable {
private final int taskId;
public IOTask(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
// 每個線程寫入自己的臨時文件
String filename = "/tmp/disk_io_test_" + taskId + ".tmp";
try (FileOutputStream fos = new FileOutputStream(filename)) {
log.info("線程-{} 正在寫入 {}", taskId, filename);
// 連續將數據寫入文件并在每次寫入后清空文件
while (!Thread.currentThread().isInterrupted()) {
performDiskIOOperation(fos, taskId);
ThreadUtil.sleep(500);
}
} catch (IOException e) {
log.error("線程-{} 發生錯誤: {}", taskId, e.getMessage());
}
}
}二、系統操作卡頓
第二個例子也同樣是用戶反饋系統操作卡頓感嚴重,整體點擊響應非常慢,我們還是考慮資源吃緊,優先使用top指令查看資源使用情況,從top指令來看:
- 輸出us查看各個核心的CPU使用率跑滿
- 系統平均負載基本超過70%(6*0.7)已經超過4.2
這就是經典的計算密集型任務跑滿所有線程的經典例子:

一般針對CPU跑滿的問題,筆者一般會通過mpstat -P ALL 1查看CPU時間片是否分配均衡,是否出現偏斜導致CPU過熱的情況,例如所有運算任務都往一個CPU核心上跑,經過筆者每秒1次的輸出持續觀察來看,整體資源吃緊,但并沒有出現資源分配偏斜的情況,同時內存資源使用率也不高,也沒有大量的iowait等待:

結合第一步top指令定位到的進程是Java進程,筆者索性通過Arthas直接定位故障代碼,首先通過thread鎖定問題線程,可以看到pool-前綴的線程基本都是跑滿單個CPU,所以我們就可以通過thread id查看線程的棧幀:

最終鎖定到了這段代碼段,即一個密集的循環運算的線程:

對應的筆者也貼出故障代碼段示例,來總結一下系統使用卡頓的排查思路:
- 基本top查看用戶態和內核態CPU使用率
- 用戶態使用率偏高,通過mpstat查看CPU使用是否偏斜,是否保證CPU親和力
- 如果CPU使用沒有出現偏斜,則直接通過問題定位到Java進程,結合Arthas快速定位問題線程進行診斷
/**
* 模擬CPU使用率過高的情況
* 通過創建多個CPU密集型任務線程來模擬高CPU使用率
*/
public static void startHighCPUUsage() {
log.info("開始模擬高CPU使用率...");
// 創建CPU密集型任務的線程池
ExecutorService cpuExecutor = Executors.newFixedThreadPool(NUM_THREADS);
// 提交多個CPU密集型任務
for (int i = 0; i < NUM_THREADS; i++) {
cpuExecutor.submit(new CPUIntensiveTask(i));
}
log.info("高CPU使用率模擬已啟動,使用 {} 個線程", NUM_THREADS);
}
/**
* CPU密集型任務,用于模擬高CPU使用率
* 該類通過執行復雜的數學計算來占用CPU資源,從而模擬高CPU使用率場景
*/
static class CPUIntensiveTask implements Runnable {
private final int taskId;
public CPUIntensiveTask(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
log.info("CPU密集型任務-{} 已啟動", taskId);
// 執行CPU密集型計算以提高CPU使用率
while (!Thread.currentThread().isInterrupted()) {
// 執行一些復雜的數學計算
double result = 0;
for (int i = 0; i < 1000000; i++) {
result += Math.sqrt(Math.log(i + 1) * Math.cos(i) * Math.sin(i));
}
log.debug("CPU任務-{} 完成一輪計算,結果: {}", taskId, result);
}
log.info("CPU密集型任務-{} 已結束", taskId);
}
}三、持續的偶發性系統卡頓問題排查
此類問題比較棘手,系統偶發卡頓意味著是瞬時、頻繁的資源吃緊,我們還是直接使用top指令無法明確立刻捕捉到進程,可能剛剛一看到飆升的進程就消失了。
同理使用mpstat、vmstat指令無法準確定位到超短期飆升問題的進程,而基于iostat也沒有看到明顯的I/O資源吃緊,所以我們可以采用perf指令解決問題,以筆者的Ubuntu服務器為例,對應的安裝步驟:
# 安裝perf工具
sudo apt install linux-tools-generic
sudo apt install linux-tools-common在筆者完成安裝并啟動之后,系統拋出WARNING: perf not found for kernel xxxx的異常,對應的解決方案是要主動安裝linux-tools-generic并定位到linux-tools目錄下找到自己的generic版本完成符號鏈接,以筆者本次安裝為例就是6.8.0-79:
sudo ln -s /usr/lib/linux-tools/6.8.0-79-generic/perf /usr/bin/perf完成上述安裝之后,我們就可以通過將頻率降低設置為99并將監控結果導出到tmp目錄下的perf.data中:
sudo perf record -F 99 -a -g -o /tmp/perf.data sleep 10可能很多讀者好奇為什么需要將頻率設置為99Hz,這樣做的目的是為了避免與系統定時器中斷頻率(通常為100Hz)同步,從而避免鎖步采樣導致的偏差。
鎖步采樣是指采樣頻率與系統定時器中斷頻率相同或成倍數關系時,采樣點會固定在相同的時間位置上,導致采樣結果不能準確反映系統整體的性能狀況。
使用99Hz這樣的素數頻率可以減少與系統周期性活動同步的概率,從而獲得更全面、更準確的性能數據。
舉個簡單的例子,若我們試圖確定道路是否出現擁堵,且通過24h一次的抽檢查,那么當前樣本就可能與交通保持一個平行的同步狀態,例如:
- 交通車流情況在每天8點-12點擁堵,而我們的程序也是恰好在每天9點采集,那么它就會認為交通情況異常擁堵
- 若每天14點進行一次采集那么就避開了交通阻塞的高峰期則會得到一個相反的、也是不正確的結論
為了規避相同周期頻率導致的lockstep即鎖同步采樣,我們可以適當降低頻率避免與交通周期時間同步,保證一天的數據能夠在一個周期內被完整地采集到,而本例最好的做法就是將定時間隔改為23h,這樣一來每個23天的樣本周期就會得到一天中所有時間的數據就能做到全面地了解到交通情況:

等待片刻后perf指令就會將結果輸出到perf.data目錄下:
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.701 MB /tmp/perf.data (586 samples) ]此時,通過sudo perf report查看報告,可以看到一個pid為1115751的Java進程對應線程CPU使用率飆升到86,此時我們就可以基于這條信息到指定的進程上查看該線程是否存在密集的運算:

最后我們也給出本示例的問題代碼:
/**
* 模擬CPU瞬間飆高然后降低的情況
* 實現每10秒一次的CPU使用率飆高和降低循環(僅使用單核)
*/
public static void startCPUSpikeAndDrop() {
log.info("開始模擬CPU瞬間飆高然后降低...");
// 創建用于CPU飆高的線程池(僅使用單核)
ExecutorService spikeExecutor = Executors.newFixedThreadPool(1);
// 提交單個CPU密集型任務來制造飆高
spikeExecutor.submit(new CPUSpikeTask(0));
log.info("CPU瞬間飆高已啟動,使用 {} 個線程", 1);
// 每隔10秒切換CPU飆高狀態,實現周期性飆高和降低
Thread spikeController = new Thread(() -> {
boolean isSpiking = true;
ExecutorService currentExecutor = spikeExecutor;
while (!Thread.currentThread().isInterrupted()) {
try {
// 等待10秒
Thread.sleep(10000);
if (isSpiking) {
// 停止當前的CPU飆高任務
currentExecutor.shutdownNow();
log.info("CPU使用率已降低");
} else {
// 啟動新的CPU飆高任務
currentExecutor = Executors.newFixedThreadPool(1);
currentExecutor.submit(new CPUSpikeTask(0));
log.info("CPU使用率已飆高");
}
// 切換狀態
isSpiking = !isSpiking;
} catch (InterruptedException e) {
log.error("CPU飆高控制線程被中斷", e);
break;
}
}
});
spikeController.setDaemon(true);
spikeController.start();
}
/**
* CPU瞬間飆高任務,用于模擬CPU瞬間飆高然后降低的情況
* 該類通過執行密集的數學計算來模擬CPU使用率的瞬時飆高,并在指定時間后自動停止
*/
static class CPUSpikeTask implements Runnable {
private final int taskId;
public CPUSpikeTask(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
log.info("CPU瞬間飆高任務-{} 已啟動", taskId);
// 執行空循環以提高CPU使用率
while (!Thread.currentThread().isInterrupted()) {
// 空循環消耗CPU
}
log.info("CPU瞬間飆高任務-{} 已結束", taskId);
}
}四、小結
本文針對應用延遲、系統卡頓、偶發頻繁卡頓三種常見的系統故障給出通用普適的排查思路,整體來說此類問題歸根結底都是系統資源吃緊,需要找到飽和的資源結合代碼推測根源并制定修復策略,以本文為例,通用的排查思路都為:
- 基于top查看CPU、內存、負載
- 若CPU未飽和則通過vmstat查看I/O資源使用情況
- 明確I/O瓶頸通過nload和iostat查詢是網絡I/O還是磁盤I/O
- 若上述排查都無果,且CPU負載偶發飆高,可通過perf并調整頻率監控系統定位系統中異常運行的資源
- 結合上述推斷結果查看是否是異常消耗,如果是則優化代碼,反之結合情況增加硬件資源

























