hello 寶兒們 周末好
我是京東碼農(nóng)小哥 ──可愛豬豬
不知道 618 剁手或者護(hù)航的你是否有新的收獲?
那么今天和大家聊聊一個話題 ──JVM 困局的攻與破
JVM 實踐對于很多新手來說有點撓破頭皮、無從下手的感覺
總覺得是一座邁不過去的大山
不過,不要緊,以后遇到 JVM 實踐上的問題與困擾
不妨先收藏這篇文章或許能夠幫助到工作中漫無頭緒的你
JVM 實踐有特定的方法論,讀完這篇文章你將有以下收獲:
- http 接口超時的常見排查思路及方法
- JVM 內(nèi)存&垃圾回收常用實踐命令
- 系統(tǒng) CPU 飆高與資源消耗的線程定位
- 如何分析 JVM 線程堆棧日志和內(nèi)存 Dump
- Excel 文件導(dǎo)出與下載的正確姿勢
- 客戶端與MQ連接超時的是是非非
以下文章閱讀預(yù)計需要 15 分鐘,目錄:
- 是什么導(dǎo)致接口超時? * 發(fā)現(xiàn)問題,初步診斷
- 根據(jù)初診,進(jìn)而確認(rèn)
- 揭開真相的面紗
- CPU何故飆升? * 起飛的 CPU,永不止步
- 找到幕后的線程
- JVM堆棧信息
- 線程與JVM堆棧信息
- 剖析JVM堆棧信息
- 繁忙的垃圾回收器? * 監(jiān)控垃圾回收
- Who侵蝕了JVM的內(nèi)存?
- JVM堆棧信息
- Excel導(dǎo)出工具選擇?
- 并發(fā)癥:鏈接MQ異常,為何?
- 總結(jié)
是什么導(dǎo)致接口超時?
其實每個程序員都是
名偵探柯南,通過蛛絲馬跡牽出大案件,今天要和大家分享一個接口超時與JVM之間的故事
- 發(fā)現(xiàn)問題,初步診斷
測試反應(yīng)線上出現(xiàn)接口超時,根據(jù)經(jīng)驗,這種情況 80%由 SQL 語句慢、鎖等待、代碼阻塞、長任務(wù)執(zhí)行等引起的接口執(zhí)行時間超出了 http 請求的超時時間所致。
以下是瀏覽器定時發(fā)起該接口的 http 請求,短則毫秒級響應(yīng)、長則 3、5 秒乃至超時。

- 根據(jù)初診,進(jìn)而確認(rèn)
遇到這種情況,例行檢查代碼,找到性能低的 SQL 語句或者代碼進(jìn)行調(diào)優(yōu)基本可以解決同類問題。
于是,便查看超時接口getTaskStatus的實現(xiàn)邏輯:
public DownloadTaskInfo getTaskStatus(String taskId) {
DownloadTaskInfo taskInfo;
// 1.獲取redis中任務(wù)的狀態(tài)
String taskInfoStr = redisUtil.get(taskId);
if (taskInfoStr == null) {
taskInfo = new DownloadTaskInfo();
taskInfo.setTaskId(taskId);
taskInfo.setStatus(DownloadTaskStatusEnum.NOT_START.getCode());
return taskInfo;
}
return JSONUtil.toBean(taskInfoStr, DownloadTaskInfo.class);
}
該接口是前端每 4 秒請求導(dǎo)出任務(wù)的執(zhí)行狀態(tài),然而并沒有發(fā)現(xiàn) SQL 語句,所以排除掉 SQL 性能所致, 唯一可疑點:redisUtil.get(taskId)訪問 redis 超時?難不成前端 4 秒請求一次 redis 把 redis 服務(wù)器性能拉低了? 或者 redis 服務(wù)被其他應(yīng)用(該 redis 多應(yīng)用公用)使用導(dǎo)致服務(wù)器性能下降? 排查各個 redis 服務(wù)器各項指標(biāo)、日志都是正常的,排查掉唯一可能由代碼或者數(shù)據(jù)庫導(dǎo)致接口超時的原因
-
揭開真相的面紗
寶兒們,是不是跟我一樣排查到此,突然有種思路 Offline 的感覺?
不知道你在使用 Windows 系統(tǒng)的時候是不是有遇到過電腦 CPU 超過 90%,連打開一個文件夾都特別慢呢?
咦?難道是服務(wù)器 CPU 所致?我們都知道 CPU 通過分配時間片來工作,接口調(diào)用過來需要 CPU 來處理,如果 CPU 繁忙,那么新進(jìn)來的任務(wù)只有等待...
CPU 何故飆升?
- 起飛的 CPU,永不止步
為了重現(xiàn)問題,點擊了系統(tǒng)的[導(dǎo)出]按鈕,瀏覽器再次發(fā)起了getTaskStatus 接口的定時調(diào)用,紅框內(nèi)為 CPU 的狀態(tài):持續(xù)飆升~

- 找到幕后的線程
CPU占用與線程的執(zhí)行是密切相關(guān)的,只有找到真正忙碌的線程,找到線程的最終目的是分析出有問題的代碼或者任務(wù)!
那么問題來了?通過什么樣的手段或者方法才能找到Top線程呢?其實很簡單,執(zhí)行以下命令:
#1.查找Java進(jìn)程Id
[admin@my-linux~]$ jps
20704 Jps
48172 Application
#2.根據(jù)進(jìn)程ID,統(tǒng)計top線程
[admin@my-linux~]$ top -Hp 48172
linux top線程命令,執(zhí)行結(jié)果如圖:

該圖反應(yīng)了幾項重要的信息:
列表中線程CPU占用排行由高到低排列。
每一行信息中有內(nèi)存占比、CPU占比等關(guān)鍵信息。
對我來說非常重要的一列:PID(線程ID)。通過PID我們可以找到對應(yīng)JVM線程堆棧信息。
-
JVM堆棧信息
什么是JVM堆棧信息?有什么用?又如何查看呢?
執(zhí)行jvm命令查看JVM堆棧信息:
[admin@my-linux~]$ jstack 48172
JVM堆棧信息是線程運行時的執(zhí)行狀態(tài),以下為堆棧日志的快照:
<pre class="custom" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`"pool-10-thread-101" #1997 prio=5 os_prio=0 tid=0x00007fc88c0d5800 nid=0xc691 waiting on condition [0x00007fbb3608a000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x0000000083e37320> (a java.util.concurrent.SynchronousQueue$TransferStack)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:458)
at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:362)
at java.util.concurrent.SynchronousQueue.take(SynchronousQueue.java:924)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)` </pre>
堆棧信息包含:
線程名稱、狀態(tài)、執(zhí)行的代碼鏈路、線程的ID對應(yīng)十六進(jìn)制等它主要目的是定位線程出現(xiàn)長時間停頓的原因,如
線程間死鎖、死循環(huán)、請求外部資源導(dǎo)致的長時間等待等。-
線程與JVM堆棧信息
通過
top -Hp命令我們找到top線程ID排行,那如何通過線程ID從JVM堆棧信息中定位到該信息呢?首先,將線程ID轉(zhuǎn)換為16進(jìn)制,比如上圖中,占用最高的線程Id:
48391和51012,轉(zhuǎn)換后分別為bd07和c744:

其次,將轉(zhuǎn)換后的16進(jìn)制在JVM堆棧信息中進(jìn)行搜索。
- 剖析JVM堆棧信息
線程Id:51012->c744,堆棧信息如下:
"pool-11-thread-1" #2039 prio=5 os_prio=0 tid=0x00007fbbfc057800 nid=0xc744 runnable [0x00007fbb35565000]
java.lang.Thread.State: RUNNABLE
at com.xxxx.utils.excel.Excel.setSheetBody(Excel.java:223)
at com.xxxx.utils.excel.Excel.toXSSFWorkbook(Excel.java:130)
at com.xxxx.utils.excel.Excel.writeOutput(Excel.java:82)
at com.xxxx.utils.excel.Excel.getExcelBytes(Excel.java:99)
該代碼邏輯是執(zhí)行excel數(shù)據(jù)拼裝,屬于正常線程占用。
而,線程Id:48391->bd07,堆棧信息如下:
"Gang worker#8 (G1 Parallel Marking Threads)" os_prio=0 tid=0x00007fcf8c1dd800 nid=0xbd07 runnable
G1垃圾回收器線程正在進(jìn)行并行標(biāo)記。同時在其他幾個top線程工作內(nèi)容也是垃圾回收相關(guān),如下圖:

由此可見,CPU飆升的根本原因是因為垃圾回收器線程一直在工作導(dǎo)致,我們的排查目標(biāo)從CPU轉(zhuǎn)移到了內(nèi)存。
繁忙的垃圾回收器
- 監(jiān)控垃圾回收
既然JVM堆棧信息中有很多G1垃圾回收線程的工作日志,那是不是垃圾回收期真的如此繁忙的工作呢?我們使用JVM命令來監(jiān)控一下吧。
[admin@my-linux~]$ jstat -gcutil 48172 1000 100
以下截圖,一行即每一秒的內(nèi)存狀況:

- E:代表新生代Eden區(qū)的空間占用比
- YGC:代表新生代的垃圾回收次數(shù)
因此,我們可以快速判斷,Eden區(qū)非??炀驼紳M,幾乎1~3秒就會觸發(fā)一次垃圾回收。實錘了,就是瘋狂的垃圾回收導(dǎo)致CPU持續(xù)飆升。
- Who侵蝕了JVM的內(nèi)存?
為了弄清楚這個原因,我們不得不使用Java的另一條風(fēng)神令:jmap。
[admin@my-linux~]$ jmap -dump:format=b,file=m.dump 48172
通過jmap導(dǎo)出了JVM內(nèi)存的dump文件,用于JVM內(nèi)存分析。文件可能會比較大,所以最好準(zhǔn)備一個稍微大一點的目錄位置來存儲。
有了dump文件,我們怎么進(jìn)行分析呢?這時候需要借助另外一個客戶端工具:Memory Analyzer Tool,可以百度自行下載。
導(dǎo)入dump文件,工具會自動分析內(nèi)存的情況,下圖1.8GB是可疑內(nèi)存:

Leak suspects查看內(nèi)存占用詳情


大部分都是poi相關(guān)的類,也就是引入的excel工具,百度發(fā)現(xiàn)poi的XSSF的數(shù)據(jù)組裝全部在內(nèi)存,如果導(dǎo)出的數(shù)據(jù)比較多,內(nèi)存會一直持續(xù)上升。
Excel導(dǎo)出工具選擇?
由于使用poi的XSSF,所有數(shù)據(jù)在全部在內(nèi)存中進(jìn)行處理,如果數(shù)據(jù)量大,將會占用極高的內(nèi)存。
因此,推薦大家使用EasyExcel,EasyExcel是一個基于Java的簡單、省內(nèi)存的讀寫Excel的開源項目。在盡可能節(jié)約內(nèi)存的情況下支持讀寫百M的Excel
并發(fā)癥:鏈接MQ異常,為何?
基于以上的分析,發(fā)現(xiàn)內(nèi)存使用不釋放導(dǎo)致垃圾回收器線程繁忙工作,進(jìn)而CPU使用率比較高。間接導(dǎo)致接口超時等現(xiàn)象。
線程無法獲取到CPU時間片,同樣也會引發(fā)其他的并發(fā)癥:與MQ等中間件鏈接異常。 一些中間件和Client端通過心跳機(jī)制維持鏈接狀態(tài)。如果Server端在指定超時時間內(nèi)未收到心跳。會造成鏈接異常、超時等并發(fā)問題。
總結(jié)
今天,由一個接口超時,逐步排查到CPU、JVM內(nèi)存、Excel工具,逐步分析出問題的根因。
工欲善其事,必現(xiàn)利其器。jstat、jstack、jmap、top、MAT等命令和工具輔助我們分析問題起到了重要作用。
同時,內(nèi)存異常也可能導(dǎo)致一系列的并發(fā)癥。
今天文章比較長,如果你都看到了這里,給可愛豬豬點個贊或者在看。
好了,今天就聊到這里,我是可愛豬豬,下篇文章再會!