JVM 故障調查教程
java 程序 cpu100%原因排查
第一步:確認cpu佔用情況及進程ID
第二步: 顯示進程下的線程佔用CPU情況
第三步: 導出java進程的線程棧
jstack 9745 > 9745.tdump
#查看tdump
cat 9745.tdump
小結
jvm 內存空間不足引起的CPU100% 原因排除
代碼
運行代碼:
java -cp demofullgc.jar -XX:+PrintGC -Xms50M -Xmx50M com.hdk.demofullgc.FullGCProblem 結果
- -Xmn 年輕代 -Xms 最小堆 默認是物理內存的1/64; -Xmx 最大堆 默認是物理內存的1/4 –Xss 棧空間,
- -XX:+UseTLAB 使用 TLAB,默認打開
- -XX:+PrintTLAB 打印 TLAB 的使用情況
- -XX:TLABSize 設置 TLAB 大小
- -XX:+DisableExplicitGC 啓用用於禁用對的調用處理的選項 System.gc()
- -XX:+PrintGC 查看 GC 基本信息
- -XX:+PrintGCDetails 查看 GC 詳細信息
- -XX:+PrintHeapAtGC 每次一次 GC 後,都打印堆信息
- -XX:+PrintGCTimeStamps啓用在每個 GC 上打印時間戳的功能
- -XX:+PrintGCApplicationConcurrentTime 打印應用程序時間(低)
- -XX:+PrintGCApplicationStoppedTime 打印暫停時長(低)
- -XX:+PrintReferenceGC 記錄回收了多少種不同引用類型的引用(重要性低)
- -verbose:class 類加載詳細過程
- -XX:+PrintVMOptions 可在程序運行時,打印虛擬機接受到的命令行顯示參數
- -XX:+PrintFlagsFinal -XX:+PrintFlagsInitial 打印所有的 JVM 參數、查看所有 JVM 參數啓動的初始值(必須會用)
- -XX:MaxTenuringThreshold 升代年齡,最大值 15, 並行(吞吐量)收集器的默認值爲 15,而 CMS 收集器的默認值爲 6。
運行:top 查看CPU佔用情況及pid
Jstat
代碼中有打印 GC 參數,生產上可以使用這個 jstat –gc 來統計
分析:jstat-gc 5953 2500 100 需要每 250 毫秒查詢一次進程 5953 垃圾收集狀況,一共查詢 100 次。
隨着GCT和GCT逐漸變大,FGC(老年代垃圾回收次數)頻率也越來越高。1,1,2,2,3,3,5,6,245。
最後拋出outofmemoryerror異常。CPU佔用100%
jmap
分析:ScheduledThreadPoolExecutor$ScheduledFutureTask,UserInfo等對象都高達159956個。主要是類FullGCProblem裏的線程池引起的。
小結
內存泄露導致回收率低
內存泄露,引用內存回收率低,而導出內存的可用空間減少,導致內存快速被佔滿垃圾回收器不斷FULLGC。最終CPU100%。
代碼
配置 -XX:+HeapDumpOnOutOfMemoryError -Xms500M -Xmx500M
內存不夠產生了oom。生成dump 文件java_pid8839.hprof。
mat分析工具
MAT 工具是基於 Eclipse 平臺開發的,本身是一個 Java 程序,是一款很好的內存分析工具。
總結:
在 JVM 出現性能問題的時候。(表現上是 CPU100%,內存一直佔用)
- 1、 如果 CPU 的 100%,要從兩個角度出發,一個有可能是業務線程瘋狂運行,比如說想很多死循環。還有一種可能性,就是 GC 線程在瘋狂的回收,因爲 JVM 中垃圾回收器主流也是多線程的,所以很容易導致 CPU 的 100%
- 2、 在遇到內存溢出的問題的時候,一般情況下我們要查看系統中哪些對象佔用得比較多,我的是一個很簡單的代碼,在實際的業務代碼中,找到對應的對象,分析對應的類,找到爲什麼這些對象不能回收的原因,就是我們前面講過的可達性分析算法,JVM 的內存區域,還有垃圾回收器的基礎。
CPU佔用過高常見原因
- 超大對象 代碼中創建了很多大對象 , 且一直因爲被引用不能被回收,這些大對象會進入老年代,導致內存一直被佔用,很容易引發 GC 甚至是 OO
- 超過預期訪問量 通常是上游系統請求流量飆升,常見於各類促銷/秒殺活動,可以結合業務流量指標排查是否有尖狀峯值。 比如如果一個系統高峯期的內存需求需要 2 個 G 的堆空間,但是堆空間設置比較小,導致內存不夠,導致 JVM 發起頻繁的 GC 甚至 OOM。 過多使用 Finalizer
- 過度使用終結器(Finalizer) 對象沒有立即被 GC,Finalizer 線程會和我們的主線程進行競爭,不過由於它的優先級較低,獲取到的 CPU 時間較少,因此 它永遠也趕不上主線程的步伐,程序消耗了所有的可用資源,最後拋出 OutOfMemoryError 異常。
- 內存泄漏 大量對象引用沒有釋放,JVM 無法對其自動回收。 長生命週期的對象持有短生命週期對象的引用 例如將 ArrayList 設置爲靜態變量,則容器中的對象在程序結束之前將不能被釋放,從而造成內存泄漏
- 變量作用域不合理: 1.一個變量的定義的作用範圍大於其使用範圍,2.沒有及時地把對象設置爲 null。
- 內部類持有外部類: Java 的非靜態內部類的這種創建方式,會隱式地持有外部類的引用,而且默認情況下這個引用是強引用,因此,如果內部類的生命週期長於外部類的生命週期,程序很容易就產生內存泄漏如果內部類的生命週期長於外部類的生命週期,程序很容易就產生內存泄漏(垃圾回收器會回收掉外部類的實例,但由於內部類持有外部類的引用,導致垃圾回收器不能正常工作) 解決方法:你可以在內部類的內部顯示持有一個外部類的軟引用(或弱引用),並通過構造方法的方式傳遞進來,在內部類的使用過程中,先判斷一下外部類是否被回收;
- Hash 值改變
- 在集合中,如果修改了對象中的那些參與計算哈希值的字段,會導致無法從集合中單獨刪除當前對象,造成內存泄露
CPU佔用高的解決策略
- 第一步:程序優化。效果通常非常大;一般運行中的程序突然產生CPU佔用高,都是因爲代碼問題引起的OOM。如果沒徹底解決代碼問題,服務多少資源都不夠用。
- 第二步:擴容。一般這種情況,出現在超過預期的訪問或業務慢慢地增加,導致資源佔用過高。
- 第三步: 參數調優。最後纔是考慮調優,通過成本、吞吐量、延遲之間找一個平衡點進行jvm的配置調優。
命令行工具
JPS
列出當前機器上正在運行的虛擬機進程,JPS 從操作系統的臨時目錄上去找(所以有一些信息可能顯示不全)
-m:輸出主函數傳入的參數. 下的 hello 就是在執行程序時從命令行輸入的參數
-l: 輸出應用程序主類完整 package 名稱或 jar 完整名稱. -v: 列出 jvm 參數, -Xms20m -Xmx50m
jstat
-printcompilation (HotSpot 編譯統計)
jinfo
VM 參數分類
JVM 的命令行參數參考:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
jmap
Heap Configuration: ##堆配置情況,也就是 JVM 參數配置的結果[平常說的 tomcat 配置 JVM 參數,就是在配置這些]
MinHeapFreeRatio = 40 ##最小堆使用比例
MaxHeapFreeRatio = 70 ##最大堆可用比例
MaxHeapSize = 2147483648 (2048.0MB) ##最大堆空間大小
NewSize = 268435456 (256.0MB) ##新生代分配大小
MaxNewSize = 268435456 (256.0MB) ##最大可新生代分配大小
OldSize = 5439488 (5.1875MB) ##老年代大小
SurvivorRatio = 8 ##新生代與 suvivor 的比例
PermSize = 134217728 (128.0MB) ##perm 區 永久代大小
MaxPermSize = 134217728 (128.0MB) ##最大可分配 perm 區 也就是永久代大小
Heap Usage: ##堆使用情況【堆內存實際的使用情況】
New Generation (Eden + 1 Survivor Space): ##新生代(伊甸區 Eden 區 + 倖存區 survior(1+2)空間)
capacity = 241631232 (230.4375MB) ##伊甸區容量
used = 77776272 (74.17323303222656MB) ##已經使用大小
free = 163854960 (156.26426696777344MB) ##剩餘容量
32.188004570534986% used ##使用比例
capacity = 214827008 (204.875MB) ##伊甸區容量
used = 74442288 (70.99369812011719MB) ##伊甸區使用
free = 140384720 (133.8813018798828MB) ##伊甸區當前剩餘容量
34.65220164496263% used ##伊甸區使用情況
capacity = 26804224 (25.5625MB) ##survior1 區容量
used = 3333984 (3.179534912109375MB) ##surviror1 區已使用情
free = 23470240 (22.382965087890625MB) ##surviror1 區剩餘容量
12.43827838477995% used ##survior1 區使用比例
capacity = 26804224 (25.5625MB) ##survior2 區容量
used = 0 (0.0MB) ##survior2 區已使用情況
free = 26804224 (25.5625MB) ##survior2 區剩餘容量
capacity = 1879048192 (1792.0MB) ##老年代容量
used = 30847928 (29.41887664794922MB) ##老年代已使用容量
free = 1848200264 (1762.5811233520508MB) ##老年代剩餘容量
1.6416783843721663% used ##老年代使用比例
-histo 打印每個 class 的實例數目,內存佔用,類全名信息.
jmap –histo jmap –histo:live 如果 live 子參數加上後,只統計活的對象數量
jmap –histo 1196 | head -20 (這樣只會顯示排名前 20 的數據 )
jmap -dump:live,format=b,file=heap.bin
Sun JDK 提供 jhat(JVM Heap Analysis Tool)命令與 jmap 搭配使用,來分析 jmap
jhat
jhat dump 文件名 後屏幕顯示"Server is ready."的提示後,用戶在瀏覽器中鍵入 http://localhost:7000/就可以訪問詳情
使用 jhat 可以在服務器上生成堆轉儲文件分析(一般不推薦,畢竟佔用服務器的資源,,比如一個文件就有 1 個 G 的話就需要大約喫一個 1G 的內存資源)
jstack
Arthas
官方文檔參考 https://alibaba.github.io/arthas/ Arthas 是 Alibaba 開源的 Java 診斷工具,深受開發者喜愛。Arthas 支持 JDK 6+,支持 Linux/Mac/Windows,採用命令行交互模式,同時提供豐富的 Tab 自動補全功能,進一步方便進行問題的定位和診斷
可視化工具
Jconsole,visualvm 這兩款使用比較簡單,一般java應用都是運行在linux平臺。所以這裏忽略了。
命令工具總結
調優之前開啓、調優之後關閉 -XX:+PrintGC 調試跟蹤之打印簡單的 GC 信息參數:
** 考慮使用** -XX:+PrintHeapAtGC, 打印推信息 參數設置: -XX:+PrintHeapAtGC應用場景: 獲取 Heap 在每次垃圾回收前後的使用狀況
-XX:+TraceClassLoading參數方法: -XX:+TraceClassLoading
應用場景:在系統控制檯信息中看到 class 加載的過程和具體的 class 信息,可用以分析類的加載順序以及是否可進行精簡操作。
-XX:+DisableExplicitGC 禁止在運行期顯式地調用 System.gc()
調優經驗分享
添加配置 -Xms1500m -Xmx1500m 增加堆內存空間 內存比例 內存指的是堆內存大小,堆內存又分爲年輕代內存和老年代內存。堆內存不足,會增加 MinorGC ,影響系統性能。
MinorGC比較頻發可以通過-Xmn 增加年輕代大小,降低 Minor GC 的頻率 。-XX:SurvivorRatio調整大survivor區來減少觸發動態年齡判斷。
-Xmn1000m -XX:SurvivorRatio=7 修改合適的大小。
-XX:MetaspaceSize= 128M -XX:MaxMetaspaceSize= 128 M 設置一個夠用值
元空間一般啓動後就不會有太多的變化,所以把MetaspaceSize和MaxMetaspaceSize設置成一樣。我們可以設定爲 128M,節約內存
吞吐量 頻繁的 GC 將會引起線程的上下文切換,增加系統的性能開銷,從而影響每次處理的線程請求,最終導致系統的吞吐量下降。
-XX:ParallelGCThreads=8 線程數可以根據你的服務器資源情況來設定(要速度快的話可以設置大點,根據 CPU 的情況來定,一般設置成 CPU 的整 數倍
延時 JVM 的 GC 持續時間也會影響到每次請求的響應時間。
-XX:+UseConcMarkSweepGC 如果是業務響應時間優先的,所以還是可以使用 CMS 垃圾回收器或者 G1 垃圾回收器。
- 響應時間優先的應用:儘可能設大,直到接近系統的最低響應時間限制(根據實際情況選擇).在此種情況下,新生代收集發生的頻率也是最小的.同時,減少到達老年代的對象.
- 吞吐量優先的應用:儘可能的設置大,可能到達 Gbit 的程度.因爲對響應時間沒有要求,垃圾收集可以並行進行,一般適合 8CPU 以上的應用.
- 避免設置過小.當新生代設置過小時會導致:1.MinorGC 次數更加頻繁 2.可能導致 MinorGC 對象直接進入老年代,如果此時老年代滿了,會觸發 FullGC.
- 響應時間優先的應用:老年代使用併發收集器,所以其大小需要小心設置,一般要考慮併發會話率和會話持續時間等一些參數.如果堆設置小了,可以會造成內存碎 片,高回收頻率以及應用暫停而使用傳統的標記清除方式;
- 如果堆大了,則需要較長的收集時間.最優化的方案,一般需要參考以下數據獲得: 併發垃圾收集信息、持久代併發收集次數、傳統 GC 信息、花在新生代和老年代回收上的時間比例。
- 吞吐量優先的應用:一般吞吐量優先的應用都有一個很大的新生代和一個較小的老年代.原因是,這樣可以儘可能回收掉大部分短期對象,減少中期的對象,而老年代盡存放長期存活對象
- 吞吐量: 這裏的衡量吞吐量是指應用程序所花費的時間和系統總運行時間的比值。我們可以按照這個公式來計算 GC 的吞吐量:系統總運行時間 = 應用程序耗時+GC 耗時。如果系統運行了 100 分鐘,GC 耗時 1 分鐘,則系統吞吐量爲 99%。GC 的吞吐量一般不能低於 95%。
- 停頓時間: 指垃圾回收器正在運行時,應用程序的暫停時間。對於串行回收器而言,停頓時間可能會比較長;而使用併發回收器,由於垃圾收集器和應用程序交替運行,程序的停頓時間就會變短,但其效率很可能不如獨佔垃圾收集器,系統的吞吐量也很可能會降低。
- 垃圾回收頻率: 通常垃圾回收的頻率越低越好,增大堆內存空間可以有效降低垃圾回收發生的頻率,但同時也意味着堆積的回收對象越多,最終也會增加回收時的停頓時間。所以我們需要適當地增大堆內存空間,保證正常的垃圾回收頻率即可
通過 JVM 參數預先設置 GC 日誌,幾種 JVM 參數設置如下:
-XX:+PrintGCDetails 輸出 GC 的詳細日誌
-XX:+PrintGCTimeStamps 輸出 GC 的時間戳(以基準時間的形式)
-XX:+PrintGCDateStamps 輸出 GC 的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在進行 GC 的前後打印出堆的信息
-Xloggc:../logs/gc.log 日誌文件的輸出路徑
java -jar -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:./gclogs jvm-1.0-SNAPSHOT.jar
日誌查看工具gcViewer,Gceasy https://gceasy.io/,
GC 調優策略
- 降低 Minor GC 頻率
- 由於新生代空間較小,Eden 區很快被填滿,就會導致頻繁 Minor GC,因此我們可以通過增大新生代空間來降低 Minor GC 的頻率。 單次 Minor GC 時間是由兩部分組成:T1(掃描新生代)和 T2(複製存活對象)。
- 降低 Full GC 的頻率:
- 由於堆內存空間不足或老年代對象太多,會觸發 Full GC,頻繁的 Full GC 會帶來上下文切換,增加系統的性能開銷 。
- 減少創建大對象:在平常的業務場景中,我們一次性從數據庫中查詢出一個大對象用於 web 端顯示。比如,一次性查詢出 60 個字段的業務操作,這種大對象如果超過年輕代最大對象閾值,會被直接創建在老年代;即使被創建在了年輕代,由於年輕代的內存空間有限,通過 Minor GC 之後也會進入到老 年代。這種大對象很容易產生較多的 Full GC。
- 增大堆內存空間:在堆內存不足的情況下,增大堆內存空間,且設置初始化堆內存爲最大堆內存,也可以降低 Full GC 的頻率。
- 選擇合適的 GC 回收器: 如果要求每次操作的響應時間必須在 500ms 以內。這個時候我們一般會選擇響應速度較快的 GC 回收器,堆內存比較小的情況下(<6G)選擇 CMS(Concurrent Mark Sweep)回收器和堆內存比較大的情況下(>8G)G1 回收器。
- GC調優小結 GC 調優是個很複雜、很細緻的過程,要根據實際情況調整,不同的機器、不同的應用、不同的性能要求調優的手段都是不同的,一般調優的思路都是"測試 - 分析 - 調優",任何調優都需要結合場景,明確已知問題和性能目標,不能爲了調優而調優,以免引入新的 Bug,帶來風險和弊端。