這幾種常見的 JVM 調優場景,你知道嗎?
假定你已經瞭解了運行時的數據區域和常用的垃圾回收算法,也瞭解了Hotspot支持的垃圾回收器。
一、cpu佔用過高
cpu佔用過高要分情況討論,是不是業務上在搞活動,突然有大批的流量進來,而且活動結束後cpu佔用率就下降了,如果是這種情況其實可以不用太關心,因為請求越多,需要處理的線程數越多,這是正常的現象。
話説回來,如果你的服務器配置本身就差,cpu也只有一個核心,這種情況,稍微多一點流量就真的能夠把你的cpu資源耗盡,這時應該考慮先把配置提升吧。
第二種情況,cpu佔用率長期過高,這種情況下可能是你的程序有那種循環次數超級多的代碼,甚至是出現死循環了。排查步驟如下:
(1)用top命令查看cpu佔用情況
這樣就可以定位出cpu過高的進程。在linux下,top命令獲得的進程號和jps工具獲得的vmid是相同的:
(2)用top -Hp命令查看線程的情況
可以看到是線程id為7287這個線程一直在佔用cpu
(3)把線程號轉換為16進制
[[email protected] ~]# printf "%x" 7287 1c77
記下這個16進制的數字,下面我們要用
(4)用jstack工具查看線程棧情況
[[email protected] ~]# jstack 7268 | grep 1c77 -A 10 "http-nio-8080-exec-2" #16 daemon prio=5 os_prio=0 tid=0x00007fb66ce81000 nid=0x1c77 runnable [0x00007fb639ab9000] java.lang.Thread.State: RUNNABLE at com.spareyaya.jvm.service.EndlessLoopService.service(EndlessLoopService.java:19) at com.spareyaya.jvm.controller.JVMController.endlessLoop(JVMController.java:30) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)
通過jstack工具輸出現在的線程棧,再通過grep命令結合上一步拿到的線程16進制的id定位到這個線程的運行情況,其中jstack後面的7268是第(1)步定位到的進程號,grep後面的是(2)、(3)步定位到的線程號。
從輸出結果可以看到這個線程處於運行狀態,在執行 com.spareyaya.jvm.service.EndlessLoopService.service
這個方法,代碼行號是19行,這樣就可以去到代碼的19行,找到其所在的代碼塊,看看是不是處於循環中,這樣就定位到了問題。
二、死鎖
死鎖並沒有第一種場景那麼明顯,web應用肯定是多線程的程序,它服務於多個請求,程序發生死鎖後,死鎖的線程處於等待狀態( WAITING
或 TIMED_WAITING
),等待狀態的線程不佔用cpu,消耗的內存也很有限,而表現上可能是請求沒法進行,最後超時了。在死鎖情況不多的時候,這種情況不容易被發現。
可以使用jstack工具來查看
(1)jps查看java進程
[[email protected] ~]# jps -l 8737 sun.tools.jps.Jps 8682 jvm-0.0.1-SNAPSHOT.jar
(2)jstack查看死鎖問題
由於web應用往往會有很多工作線程,特別是在高併發的情況下線程數更多,於是這個命令的輸出內容會十分多。jstack最大的好處就是會把產生死鎖的信息(包含是什麼線程產生的)輸出到最後,所以我們只需要看最後的內容就行了
Java stack information for the threads listed above: =================================================== "Thread-4": at com.spareyaya.jvm.service.DeadLockService.service2(DeadLockService.java:35) - waiting to lock <0x00000000f5035ae0> (a java.lang.Object) - locked <0x00000000f5035af0> (a java.lang.Object) at com.spareyaya.jvm.controller.JVMController.lambda$deadLock$1(JVMController.java:41) at com.spareyaya.jvm.controller.JVMController$$Lambda$457/1776922136.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) "Thread-3": at com.spareyaya.jvm.service.DeadLockService.service1(DeadLockService.java:27) - waiting to lock <0x00000000f5035af0> (a java.lang.Object) - locked <0x00000000f5035ae0> (a java.lang.Object) at com.spareyaya.jvm.controller.JVMController.lambda$deadLock$0(JVMController.java:37) at com.spareyaya.jvm.controller.JVMController$$Lambda$456/474286897.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) Found 1 deadlock.
發現了一個死鎖,原因也一目瞭然。
三、內存泄漏
我們都知道,java和c++的最大區別是前者會自動收回不再使用的內存,後者需要程序員手動釋放。在c++中,如果我們忘記釋放內存就會發生內存泄漏。但是,不要以為jvm幫我們回收了內存就不會出現內存泄漏。
程序發生內存泄漏後,進程的可用內存會慢慢變少,最後的結果就是拋出OOM錯誤。發生OOM錯誤後可能會想到是內存不夠大,於是把-Xmx參數調大,然後重啟應用。這麼做的結果就是,過了一段時間後,OOM依然會出現。最後無法再調大最大堆內存了,結果就是隻能每隔一段時間重啟一下應用。
內存泄漏的另一個可能的表現是請求的響應時間變長了。這是因為頻繁發生的GC會暫停其它所有線程( Stop The World
)造成的。
為了模擬這個場景,使用了以下的程序
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Main { public static void main(String[] args) { Main main = new Main(); while (true) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } main.run(); } } private void run() { ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { executorService.execute(() -> { // do something... }); } } }
運行參數是 -Xms20m -Xmx20m -XX:+PrintGC
,把可用內存調小一點,並且在發生gc時輸出信息,運行結果如下
[GC (Allocation Failure) 12776K->10840K(18432K), 0.0309510 secs] [GC (Allocation Failure) 13400K->11520K(18432K), 0.0333385 secs] [GC (Allocation Failure) 14080K->12168K(18432K), 0.0332409 secs] [GC (Allocation Failure) 14728K->12832K(18432K), 0.0370435 secs] [Full GC (Ergonomics) 12832K->12363K(18432K), 0.1942141 secs] [Full GC (Ergonomics) 14923K->12951K(18432K), 0.1607221 secs] [Full GC (Ergonomics) 15511K->13542K(18432K), 0.1956311 secs] ... [Full GC (Ergonomics) 16382K->16381K(18432K), 0.1734902 secs] [Full GC (Ergonomics) 16383K->16383K(18432K), 0.1922607 secs] [Full GC (Ergonomics) 16383K->16383K(18432K), 0.1824278 secs] [Full GC (Allocation Failure) 16383K->16383K(18432K), 0.1710382 secs] [Full GC (Ergonomics) 16383K->16382K(18432K), 0.1829138 secs] [Full GC (Ergonomics) Exception in thread "main" 16383K->16382K(18432K), 0.1406222 secs] [Full GC (Allocation Failure) 16382K->16382K(18432K), 0.1392928 secs] [Full GC (Ergonomics) 16383K->16382K(18432K), 0.1546243 secs] [Full GC (Ergonomics) 16383K->16382K(18432K), 0.1755271 secs] [Full GC (Ergonomics) 16383K->16382K(18432K), 0.1699080 secs] [Full GC (Allocation Failure) 16382K->16382K(18432K), 0.1697982 secs] [Full GC (Ergonomics) 16383K->16382K(18432K), 0.1851136 secs] [Full GC (Allocation Failure) 16382K->16382K(18432K), 0.1655088 secs] java.lang.OutOfMemoryError: Java heap space
可以看到雖然一直在gc,佔用的內存卻越來越多,説明程序有的對象無法被回收。但是上面的程序對象都是定義在方法內的,屬於局部變量,局部變量在方法運行結果後,所引用的對象在gc時應該被回收啊,但是這裏明顯沒有。
為了找出到底是哪些對象沒能被回收,我們加上運行參數 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.bin
,意思是發生OOM時把堆內存信息dump出來。運行程序直至異常,於是得到 heap.dump
文件,然後我們藉助eclipse的MAT插件來分析,如果沒有安裝需要先安裝。
然後 File->Open Heap Dump...
,然後選擇剛才 dump
出來的文件,選擇 Leak Suspects
MAT會列出所有可能發生內存泄漏的對象
可以看到居然有 21260
個Thread對象,3386個 ThreadPoolExecutor
對象,如果你去看一下 java.util.concurrent.ThreadPoolExecutor
的源碼,可以發現線程池為了複用線程,會不斷地等待新的任務,線程也不會回收,需要調用其 shutdown
方法才能讓線程池執行完任務後停止。
其實線程池定義成局部變量,好的做法是設置成單例。
上面只是其中一種處理方法
在線上的應用,內存往往會設置得很大,這樣發生OOM再把內存快照dump出來的文件就會很大,可能大到在本地的電腦中已經無法分析了(因為內存不足夠打開這個dump文件)。這裏介紹另一種處理辦法:
(1)用jps定位到進程號
C:\Users\spareyaya\IdeaProjects\maven-project\target\classes\org\example\net>jps -l 24836 org.example.net.Main 62520 org.jetbrains.jps.cmdline.Launcher 129980 sun.tools.jps.Jps 136028 org.jetbrains.jps.cmdline.Launcher
因為已經知道了是哪個應用發生了OOM,這樣可以直接用jps找到進程號 135988
(2)用jstat分析gc活動情況
jstat是一個統計java進程內存使用情況和gc活動的工具,參數可以有很多,可以通過 jstat -help
查看所有參數以及含義
C:\Users\spareyaya\IdeaProjects\maven-project\target\classes\org\example\net>jstat -gcutil -t -h8 24836 1000 Timestamp S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 29.1 32.81 0.00 23.48 85.92 92.84 84.13 14 0.339 0 0.000 0.339 30.1 32.81 0.00 78.12 85.92 92.84 84.13 14 0.339 0 0.000 0.339 31.1 0.00 0.00 22.70 91.74 92.72 83.71 15 0.389 1 0.233 0.622
上面是命令意思是輸出gc的情況,輸出時間,每8行輸出一個行頭信息,統計的進程號是 24836
,每1000毫秒輸出一次信息。
輸出信息是 Timestamp
是距離jvm啟動的時間,S0、S1、E是新生代的兩個 Survivor
和 Eden
,O是老年代區,M是 Metaspace
,CCS使用壓縮比例,YGC和YGCT分別是新生代gc的次數和時間, FGC
和 FGCT
分別是老年代gc的次數和時間,GCT是gc的總時間。雖然發生了gc,但是老年代內存佔用率根本沒下降,説明有的對象沒法被回收(當然也不排除這些對象真的是有用)。
(3)用jmap工具dump出內存快照
jmap可以把指定java進程的內存快照dump出來,效果和第一種處理辦法一樣,不同的是它不用等OOM就可以做到,而且dump出來的快照也會小很多。
jmap -dump:live,format=b,file=heap.bin 24836
這時會得到 heap.bin
的內存快照文件,然後就可以用eclipse來分析了。
四、總結
以上三種嚴格地説還算不上jvm的調優,只是用了jvm工具把代碼中存在的問題找了出來。我們進行jvm的主要目的是儘量減少停頓時間,提高系統的吞吐量。
但是如果我們沒有對系統進行分析就盲目去設置其中的參數,可能會得到更壞的結果,jvm發展到今天,各種默認的參數可能是實驗室的人經過多次的測試來做平衡的,適用大多數的應用場景。
如果你認為你的jvm確實有調優的必要,也務必要取樣分析,最後還得慢慢多次調節,才有可能得到更優的效果。
- 天翼雲全場景業務無縫替換至國產原生操作系統CTyunOS!
- 以羊了個羊為例,淺談小程序抓包與響應報文修改
- 這幾種常見的 JVM 調優場景,你知道嗎?
- 如此狂妄,自稱高性能隊列的Disruptor有啥來頭?
- 為什麼要學習GoF設計模式?
- 827. 最大人工島 : 簡單「並查集 枚舉」運用題
- 手把手教你如何使用 Timestream 實現物聯網時序數據存儲和分析
- 850. 矩形面積 II : 掃描線模板題
- Java 併發編程解析 | 基於JDK源碼解析Java領域中的併發鎖,我們可以從中學習到什麼內容?
- 【手把手】光説不練假把式,這篇全鏈路壓測實踐探索
- 大廠鍾愛的全鏈路壓測有什麼意義?四種壓測方案詳細對比分析
- 寫個續集,填坑來了!關於“Thread.sleep(0)這一行‘看似無用’的代碼”裏面留下的坑。
- 857. 僱傭 K 名工人的最低成本 : 枚舉 優先隊列(堆)運用題
- Vue3 實現一個自定義toast(小彈窗)
- 669. 修剪二叉搜索樹 : 常規樹的遍歷與二叉樹性質
- 讀完 RocketMQ 源碼,我學會了如何優雅的創建線程
- 性能調優——小小的log大大的坑
- 1582. 二進制矩陣中的特殊位置 : 簡單模擬題
- elementui源碼學習之仿寫一個el-switch
- 646. 最長數對鏈 : 常規貪心 DP 運用題