一、CPU使用率過高的原因一般是:
1、線程空耗,如大量線程獲取鎖的過程中自旋等待;
2、系統進行密集運算(密集數學運算的AI程序等);
3、內存不足造成JVM頻繁fullGC;
4、系統正在進行大量線程上下文切換消耗資源;
線程相關問題使用jstack指令(java虛擬機自帶的指令)獲取JVM中指定進程的當前所有線程的堆棧跟蹤信息。
二、jstack介紹
1、用法: jstack [ options ] pid
2、作用
1)jstack命令用於打印指定Java進程、核心文件或遠程調試服務器的Java線程的Java堆棧跟蹤信息。
2)jstack命令可以生成JVM當前時刻的線程快照。線程快照是當前JVM內每一條線程正在執行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現長時間停頓的原因,如線程間死鎖、死循環、請求外部資源導致的長時間等待等。
3)如果java程序崩潰生成core文件,jstack工具可以用來獲得core文件的java stack和native stack的信息,從而可以輕鬆地知道java程序是如何崩潰和在程序何處發生問題。
3、jstack生成的線程調用堆棧快照中的線程狀態
- NEW 未啓動的,不會出現在Dump中;
- RUNNABLE 在虛擬機內執行的;
- BLOCKED 受阻塞並等待監視器鎖;
- WATING 無限期等待另一個線程執行特定操作;
- TIMED_WATING 有時限的等待另一個線程的特定操作;
- TERMINATED 已退出的;
4、jstack日誌分析要點
1)查看具有相同堆棧跟蹤的線程;
當應用程序中存在性能瓶頸時,大多數線程將開始在該有問題的瓶頸區域上累積。這些線程將具有相同的堆棧跟蹤。因此,每當大量線程表現出相同/重複的堆棧跟蹤時,就應該研究這些堆棧跟蹤。這可能存在性能問題。
以下是一些方案:
- 假設外部服務正在變慢,那麼大量線程將開始等待其響應。在這種情況下,這些線程將表現出相同的堆棧跟蹤。
- 假設一個線程獲得了一個鎖,它從未被釋放過,那麼處於相同執行路徑的其他幾個線程將進入阻塞狀態,表現出相同的堆棧跟蹤。
- 如果循環(for 循環, while 循環, do..while 循環) 條件不會終止,則執行該循環的多個線程將呈現相同的堆棧跟蹤。
eg:
此應用程序運行正常,但突然間變得無響應。獲取來自此應用程序的dump。顯示400個線程中的225個線程表現出相同的堆棧跟蹤:
從堆棧跟蹤中可以推斷出線程被阻止並等待對象上的鎖定0x00000006afaa5a60。225 個這樣的線程正在等待獲得同一對象上的鎖定。這絕對是一個不好的跡象,說明線程匱乏。
這個鎖是由“ajp-bio-192.168.100.128-9022-exec-84”持有的。下面是堆棧跟蹤此線程。此線程0x00000006afaa5a60獲取了對象上的鎖,但在獲取鎖後,卡住了等待數據庫的響應。對於此應用程序數據庫,未設置超時。因此,此線程的數據庫調用再也沒有返回。由於這個原因,其他225個線程被卡住了。因此,應用程序變得無響應。
設置正確的數據庫超時值後,此問題消失了。
2)如果應用程序佔用大量 CPU,重點查看處於 run狀態的線程
3)是否存在阻塞線程,阻止其他線程的線程顯示在此處。阻塞線程使應用程序無響應。
線程A可能已獲取鎖 1,但永遠不會釋放它。線程B可能已獲取鎖 2 並等待此鎖 1。線程C可能正在等待獲取鎖 2。線程之間的這種傳遞塊可能會使整個應用程序無響應。
補充:Monitor
Monitor是 Java中用以實現線程之間的互斥與協作的主要手段,它可以看成是對象或者 Class的鎖。每一個對象都有,也僅有一個 monitor。下 面這個圖,描述了線程和 Monitor之間關係,以及線程的狀態轉換圖:
進入區(Entry Set):表示線程通過synchronized要求獲取對象的鎖。如果對象未被鎖住,則進入擁有者,否則則在進入區等待。一旦對象鎖被其他線程釋放,立即參與競爭。
擁有者(The Owner):表示某一線程成功競爭到對象鎖。
等待區(Wait Set):表示線程通過對象的wait方法,釋放對象的鎖,並在等待區等待被喚醒。
從圖中可以看出,一個 Monitor在某個時刻,只能被一個線程擁有,該線程就是 “Active Thread”,而其它線程都是 “Waiting Thread”,分別在兩個隊列 “ Entry Set”和 “Wait Set”裏面等候。在“Entry Set”中等待的線程狀態是“Waiting for monitor entry”,而在 “Wait Set”中等待的線程狀態是 “in Object.wait()”。 先看 “Entry Set”裏面的線程。稱被 synchronized保護起來的代碼段爲臨界區。當一個線程申請進入臨界區時,它就進入了 “Entry Set”隊列。
4)GC垃圾處理器
根據所使用的 GC 算法類型(串行、並行、G1、CMS),將創建默認數量的垃圾回收線程。有時,會根據默認配置創建太多無關的GC線程。太多的 GC 線程也會影響應用程序的性能。因此,應仔細配置GC線程計數。
4.1)並行垃圾處理器
如果使用並行 GC 算法,則 GC 線程數由 -XX:並行 GC 線程屬性控制。 Linux/x86 計算機上的 -XX:ParallelGCThreads默認值是根據以下公式派生的:
因此,如果JVM在具有32個處理器的服務器上運行,則ParallelGCThread值將爲:23(即8 + (32 – 8)*(5/8))。
4.2)CMS垃圾收集器
如果使用 CMS GC 算法,則 GC 線程數由 -XX:並行線程和 -XX:連接線程屬性控制。默認值 -XX:康格線程數是根據以下公式派生的:
最大((並行線程+2)/4, 1)因此,如果JVM在具有32個處理器的服務器上運行,那麼
- ParallelGCThread值將爲:23(即 8 + (32 – 8)*(5/8))
- ConcGCThreads值將是:6。
- 因此,總 GC 線程數爲:29(即ParallelGCThread 數 + ConcGCThreads ,即 23 + 6)
4.3)G1垃圾收集器
如果使用 G1 GC 算法,則 GC 線程數由 -XX:並行線程、-XX:連接線程、-XX:G1限制線程屬性控制。默認值 -XX:G1組件無限線程是根據以下公式派生的:
並行線程數+1,因此,如果JVM在具有32個處理器的服務器上運行,那麼
- ParallelGCThread值將爲:23(即 8 + (32 – 8)*(5/8))
- ConcGCThreads值將爲:6
- G1ConcRefinementThreads定義線程值將爲24(即23 + 1)
- 因此,總 GC 線程數爲:53(即ParallelGCThread 數 + ConcGCThreads + G1ConcRefinementThreads,即 23 + 6 + 24)
GC 的 53 個線程是一個相當大的數字。應適當調整。
5)堆棧溢出
堆棧溢出漏洞的解決方案是什麼?
1. 修復代碼
由於非終止遞歸調用等,線程堆棧大小可以增長到很大的大小。在這種情況下,必須修復導致遞歸循環的源代碼。當拋出“堆棧溢出錯誤”時,它將打印它以遞歸方式執行的代碼的堆棧跟蹤。此代碼是開始調試和修復問題的良好指針。
2. 增加線程堆棧大小 (-Xs)
可能存在需要增加線程堆棧大小的正當理由。也許線程必須執行大量方法或大量局部變量/在線程一直在執行的方法中創建。在這種情況下,您可以使用 JVM 參數“-Xss”來增加線程的堆棧大小。啓動應用程序時需要傳遞此參數。
6)死鎖問題
從上面的線程dump中,可以看到
- “線程 4”正在等待由“線程 0”持有的0x00000007ac3b2718
- “線程 0”正在等待由“線程 1”保存0x00000007ac3b2748
- “線程 1”正在等待由“線程 2”保存的0x00000007ac3b2778
- “線程 2”正在等待由“線程 3”持有的0x00000007ac3b27a8
- “線程 3”正在等待由“線程 4”持有的0x00000007ac3b27d8
7)、妖精陷阱
在垃圾回收過程中,具有 finalizize()方法的對象與沒有它們的對象處理方式不同。在垃圾回收階段,具有 finalize() 的對象不會立即從內存中逐出。相反,作爲第一步,這些對象被添加到 java.lang.ref.終結器對象的內部隊列中。有一個名爲“Finalizer”的低優先級 JVM 線程,它執行隊列中每個對象的 finalize() 方法。只有在執行 finalizize() 方法之後,對象纔有資格進行垃圾回收。由於 finalizize()方法的實現不佳,如果Finalizer線程被阻止,將對 JVM 產生嚴重的級聯影響。 java.lang.ref.Finalize 的內部隊列將開始增長,導致JVM的內存消耗迅速增長,導致內存不足錯誤,危及整個JVM的可用性。因此,在分析線程dump時,強烈建議研究Finalizer線程的堆棧跟蹤。
eg:在 finalizize() 方法中被阻止的Finalizer線程的示例堆棧跟蹤
5、jstack日誌日誌分析
刷新某個頁面造成CPU使用率耗盡的jstack日誌如下:
日誌分析:
5.1、基礎知識點梳理
5.1.1、通過日誌可以看到大量線程處於waiting on condition狀態,該狀態出現在線程等待某個條件的發生;
1)最常見的情況是線程在等待網絡的讀寫數據(即進行網絡IO),比如當網絡數據沒有準備好讀時,線程處於這種等待狀態。而一旦有數據準備好讀之後,線程會重新激活,讀取並處理數據;
2)正等待網絡讀寫,這可能是一個網絡瓶頸的徵兆,因爲網絡阻塞導致線程無法執行,原因有兩種:
一種情況是網絡非常忙,幾乎消耗了所有的帶寬,仍然有大量數據等待網絡讀寫;
另一種情況也可能是網絡空閒,但由於路由等問題,導致包無法正常的到達;
3)該線程在 sleep,等待 sleep的時間到了時候,將被喚醒。
推薦閱讀https://zhuanlan.zhihu.com/p/475571849
5.1.2、僞運行狀態
處於“可運行”狀態的線程會消耗 CPU。因此,當分析線程dump以消除高 CPU 消耗時,應徹底檢查處於“可運行”狀態的線程。通常在線程dump中,多個線程被分類爲“RUNNABLE”狀態。但實際上,一部分實際上並沒有RUN狀態,而是在等待。但是,JVM仍然將它們歸類爲“可運行”狀態。
eg:
在這些堆棧跟蹤中,線程實際上並不處於“可運行”狀態。即沒有主動執行任何代碼。只是在等待套接字讀取或寫入。因爲JVM並不真正知道線程的native方法中正在做什麼,JVM將它們分類爲“RUNNABLE”狀態。實際運行的線程將消耗 CPU,而這些線程處於 I/O 等待狀態,不消耗任何 CPU。
5.2、通過可視化工具分析(推薦在線分析網站https://fastthread.io/ft-index.jsp)
1)使用top指令查看內存正常;
2)分析GC線程狀況
GC線程數正常;
3)查看是否存在阻塞線程
檢測結果未發現異常。
4)常見情況下的異常檢測(死鎖、終結器線程等)
如圖可知,並未發現異常情況;
5)排除了以上情況,說明沒有明顯的代碼護或者環境配置異常,對於CPU消耗過高的情況,分析RUN狀態的線程
查看cpu佔用最高的線程的pid
top -Hp pid
根據進程號查看堆棧信息
jstack 1 | grep -A 10 "0x88"(線程號轉爲16進制)
對比兩個堆棧日誌發現,佔用CPU高的線程是數據庫進行網絡IO線程,線程正在等待系統調用epoll實例事件。
補充:
epoll_wait()系統調用等待文件描述符epfd引用的epoll實例上的事件。事件所指向的存儲區域將包含可供調用者使用的事件。 epoll_wait()最多返回最大事件。 maxevents參數必須大於零。 timeout參數指定epoll_wait()將阻止的最小毫秒數。 指定超時值爲-1會導致epoll_wait()無限期阻塞,而指定的超時時間等於零導致epoll_wait()立即返回,即使沒有可用事件。
火焰圖
確定問題原因,排查刷新頁面時的日誌中的sql,從數據庫執行發現有髒數據導致的一個sql執行時一直不返回查詢結果,數據進行清洗,測試,系統正常。
推薦相關文章:
https://blog.csdn.net/lsz137105/article/details/104644396
https://blog.csdn.net/zhoumuyu_yu/article/details/112476477
感謝閱讀,借鑑了不少大佬資料,如需轉載,請註明出處,謝謝!https://www.cnblogs.com/huyangshu-fs/p/16718553.html