經驗整理-1-JVM調優舉例-100-@

-------------------JVM調優實踐-------------

?jconsole連接不上失敗?

添加:vm optiong=-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false

導致GC出現問題的場景原因及具體現象

巧記:堆、方法區

導致GC出現問題的場景有: 
(堆)創建可達有用的大對象:這個是存儲在堆中的,需要控制好對象的數量和大小,尤其是大的對象很容易進入老年代
(堆)創建可達無用的大對象(內存溢出):這個是存儲在堆中的,需要控制好對象的數量和大小,尤其是大的對象很容易進入老年代
(堆)全局集合(很容易造成內存溢出):全局集合通常是生命週期比較長的,因此需要特別注意全局集合的使用
(堆)緩存:緩存選用的數據結構不同,會很大程序影響內存的大小和gc
(棧)棧空間不足,創建線程數比較多超過剩餘內存(還有一種是單個線程內遞歸調用報StackOverflowError大家都把它算進OOM範疇了)----1M對應線程數3000~5000
(方法區)ClassLoader:主要是ClassLoader動態加載類新增類信息,容易造成永久代內存不足
(方法區)常量池過大
(本地內存)多線程:線程分配會佔用本地內存,過多的線程也會造成內存不足
 
以上使用不當很容易造成具體現象是: 
一、頻繁GC -> STW--Stop the world,使你的應用響應變慢,停頓卡頓嚴重,CPU使用率過高 (可達有用的大對象和內存溢出)
二 、OOM,直接造成內存溢出錯誤使得程序退出。OOM又可以分爲以下幾種:
Heap space:堆內存不足
PermGen space:永久代內存不足(
ClassLoader新增類信息/常量池過大)
Native thread:本地線程沒有足夠內存可分配

JVM調優的選擇:

JVM調優主要涉及:
一、堆大小的設置
二、GC收集器的選擇:

1)(GC每次吞的多執行的多)吞吐量大大應用,一般採用並行收集,開啓多個線程,加快gc的回收。---並行收集器(UseParallelGC)
2)(用戶體驗)-響應速度高的應用,一般採用併發收集,比如應用服務器。--新生代(ParNew)老年代(UseConcMarkSweepGC)
 CMS是標記清理算法,因此會有碎片,建議設置以下參數---性能調優配參必備:

三、代碼BUG

性能調優配參必備:

-XX:+UseConcMarkSweepGC   #併發收集年老代,配置這個以後,-XX:NewRatio=4的配置失效,所以,此時年輕代大小最好用-Xmn設置。
-XX:+CMSScavengeBeforeRemark  //在執行CMS remark之前進行一次youngGC,這樣能有效降低remark的時間
-XX:CMSInitiatingOccupancyFraction=80 # 表示年老代空間到80%時就開始執行CMS
-XX:+UseCMSCompactAtFullCollection # 打開對年老代的壓縮。可能會影響性能,但是可以消除內存碎片同,--要和下面這個組合用,控制它爲多少次CMS才執行一次整理,可以不至於太影響性能
-XX:CMSFullGCsBeforeCompaction=10 # 由於併發收集器不對內存空間進行壓縮、整理,所以運行一段時間以後會產生“碎片”,使得運行效率降低。此參數設置運行10次FullGC以後對內存空間進行壓縮、整理。 

 

調優調哪些東西

(和上面有點重複)

一、堆棧內存的分配的調優;
二、
GC回收器的選擇調優;
三、JVM本身調優沒有明顯變化,考慮代碼本身的調優,是否有BUG,效率很低的寫法,
代碼BUG
,比如,
1)大查詢SQL();循環裏頻繁創建對象();只在一個方法內用到的對象,指給了成員變量對象引用
-----堆相關問題------
代碼問題:
1)頻繁創建大量存活對象,很快弄滿eden,會頻繁MinorGC,這種對象快速進入survivor,s0,s1也會頻繁的相互複製,很快survivor滿了或對象年齡,比較快的進入老年代,等老年代到達閥值,引起fullGC(卡頓);;;;-----
2)頻繁創建超大存活對象,比如長數組,長字符串,達到閥值或MinorGC時survivor已裝不下滿了,直接進入老年代,等老年代到達閥值,引起fullGC(卡頓);
3)以上兩種情況,頻繁創建超大存活對象或大量存活對象,如果長時間存活沒有釋放,就會內存不足分配新空間,進入fullGC後,引起OOM(OOM內存溢出);---1、大查詢SQL---解決:分批查詢且解決循環查詢BUG
內存分配問題:
1)xmn和Xms,最大堆大小和默認堆大小設置的太小了,導致fullGC後依然沒有空間(OutOfMemoryError堆棧溢出)
1.1)xmn,分配的新生代內存太小了,新創建的對象還存活,導致fullGC(OutOfMemoryError堆棧溢出)
1.2)xmn和Xms,最大堆大小和默認堆大小設置的太小了,導致fullGC(OutOfMemoryError堆棧溢出)
2)在 CMS 啓動過程中,老年代碎片化嚴重,無法容納新生代提升上來的大對象(OutOfMemoryError堆棧溢出)----
-----棧相關問題------
代碼問題:
1)方法深度遞歸(StackOverflowError棧的最大深度1000-2000);;;;-----
內存分配問題:
1)Xss太小了(OutOfMemoryError棧溢出)


--------------------------------我們怎麼操作調優---------------------------

-------------工具選擇----------------------------------------------------------
jvisualvm分析程序死鎖,用它的VisualGc插件來實時監控生產GC內存變化(看GC日誌只是還原這個GC回收過程的內存變化,GC次數、GC時長需要計算一下)、監視標籤裏還能導出現場堆dump文件(dump文件裏能看到類實例對象的大小佔比找出大對象--如JVM內存溢出、死鎖能直接分析出),另外也能把生產的dump文件導入,找出佔比高的大對象
gceasy:用來分析生產GC日誌,查內存大小分配比例,查GC回收過程的的內存變化、吞吐量、最大最小停頓時間、普通GC和fullGC的GC耗時時長、GC次數、查找內存泄漏
 

MAT(Eclipse也有插件):直觀給我們展示內存信息,能提供一些重要的信息:

  • 所有的對象信息,包括對象實例、成員變量、存儲於棧中的基本類型值和存儲於堆中的其他對象的引用值。
  • 所有的類信息,包括classloader、類名稱、父類、靜態變量等
  • GCRoot到所有的這些對象的引用路徑
  • 線程信息,包括線程的調用棧及此線程的線程局部變量(TLS)
  • 內存泄露的定位與分析:問題重現後纔可以進行分析
     

-------------怎麼判斷GC存在問題----判斷標準------------------------------------
耗時時長:長於50毫秒與1000毫秒
週期頻率:小於10秒一次和10分鐘一次

-------------服務啓動配置JVM參數常識------------------------------------
提前把該加的JVM參數配置加上,比如GC日誌打印、堆dump文件等,方便異常出現能收集到數據,(catalina.sh文件的啓動參數export JAVA_OPTS=)
 GC日誌打印:-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/data/gc.log)。
 堆dump文件:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\dump\   
(堆dump太大了,用命令式不知道有沒有風險,如果能即使處理不會丟失,還是建議用命令輸出堆dump文件jmap -dump:format=b,file=文件名 [pid]

案例1-fullgc排查---fullgc次數達到運維報警---停頓卡頓嚴重,系統越來越慢,CPU使用率過高

---------------頻繁fullgc,造成系統卡頓嚴重,通過GC日誌、堆dump文件來幫助我們定位,查找不容易發現的代碼本身的問題---
一、 現象:-zabbix監控報警,CPU資源佔用過高,最近並沒有特別開發任務上線,但是最近有發佈過一個新的功能。
二、分析問題,可能原因----巧記:老新永久系統,fanalize執行垃圾碎片,還得看代碼
1、老年代不足 (分幾種:一、代碼問題,頻繁創建了大對象或者部分內存泄漏;二、比如新生代太小裝不下,普通GC頻繁,導致S空間不足及年齡增長加快,進入老年代的速度也加快,容易引起fullGC;三、再比如老年代太小,創建了一些大對象直接引起老年代fullGC)
2、 System.gc()方法的調用
3、 永久代不足
4、執行CMS GC的過程中同時有對象要放入老年代,因老年代浮動垃圾碎片空間臨時不足引起Full GC(concurrent mode failure)
5、minor gc時年輕代的存活區空間不足而晉升老年代,老年代又空間不足而觸發full gc
6、應用程序過度使用 finalize()。
三 、去證實問題,拿GC日誌,生成堆棧dump文件-----把gc日誌下載下來,對jvm進行jmap dump命令生成dump文件,通過工具分析。
具體如下:

---測試環境版---
第一步:用top命令找出佔用CPU較高的java應用程序進程pid  
第二步:使用top -H -p pid,查看該進程裏佔用CPU較高的線程ID 
第三步:把得到的線程ID轉成16進制(echo 'obase=16;thread_id'|bc)
第四步:打印出jvm實例的線程堆棧(jstack pid),在線程堆棧裏找出線程ID對應的代碼塊,開始優化吧! 

太難搞命令了,測試環境用工具實時監控吧:jvisualvm分析程序死鎖,用它的VisualGc插件來實時監控生產GC內存變化(看GC日誌只是還原這個GC回收過程的內存變化,GC次數、GC時長需要計算一下)、監視標籤裏還能導出現場堆dump文件(dump文件裏能看到類實例對象的大小佔比找出大對象--如JVM內存溢出、死鎖能直接分析出),另外也能把生產的dump文件導入,找出佔比高的大對象


---生產環境版---
讓運維幫忙把gc日誌下載下來,對jvm進行jmap的dump命令生成堆dump文件(jmap -dump:format=b,file=文件名 [pid]),jstack的dump命令生成線程dump文件發給我(jstack 1316 |grep -i http-8001 -A 10  >/home/jstack.txt)。(gzip命令壓縮很強大)
通過工具分析:
1)GC日誌---gceasy:用來分析生產GC日誌,查內存大小分配比例,查GC回收過程的的內存變化、吞吐量、最大最小停頓時間、普通GC和fullGC的GC耗時時長、GC次數、查找內存泄漏
2)堆dump文件---jvisualvm:把生產的dump文件導入,推dump文件在類標籤那裏,能找出佔比高的大對象

結論-----------------最後會發現幾種可能: 1、代碼頻繁創建了大對象或者部分內存泄漏(如果全內存泄漏就會GC回收不了,OOM且一直卡住),直接或15歲進入了老年代,引起的FULLGC,比如查詢大SQL。2、新生代內存太小,頻繁GC,加速進入老年代,引起的FULL gc. 3.老年代本來就小


案例1-舉例:系統併發執行對賬fullGC頻繁,優化後恢復正常。
導致原因: 
系統中有一個查詢50000筆交易的SQL,SQL條件漏傳;被循環執行了10000次。上線的代碼有點多,查代碼沒查出來哪問題。
解決辦法: 
先分析gc,最後---SQL條件加上,就沒有50000筆了。

案例2-內存泄露排查---易引起的fullgc或嚴重點造成內存溢出oom--

內存泄露:對象無用了,但仍然可達(未釋放),垃圾回收器無法回收---如果有人問oom也可以馬這個加強一點當成答案,剛好溢出就是OOM
舉例
很多情況:1、全局的容器類(如公用的集合HashMap放元素後業務異常了,忘記刪元素)2、像Runnable對象沒釋放 3、原子數據類

---------------頻繁fullgc,造成系統卡頓嚴重,通過GC日誌、堆dump文件來幫助我們定位,查找不容易發現的代碼本身的問題---
一、 現象:-zabbix監控報警,CPU資源佔用過高,最近並沒有特別開發任務上線,但是最近有發佈過一個新的功能。
二、分析問題,可能原因----集合HashMap放元素後業務異常了,忘記刪元素

---生產環境版------使用jVisualvm分析內存泄漏,gceasy作輔助
1)jmap命令拿到對應的堆dump文件(按照程序輸出分開進行堆dump多次,好比較),同時下載GC日誌。(如果是本地或遠端測試環境可自已監控標籤生成dump文件)
2)先把GC日誌導入gceasy分析一下,發現老年代回收頻繁,但是內存並沒有明顯減少,說明存在無法被回收的可達對象,可能是內存泄漏了。
3)把dump文件導入jVisualvm分析一下,進入“類”標籤,點擊“與另一個堆dump文件比較”按扭,把兩個堆dump進行比較
比較結果如下:
可以看出在兩次間隔時間內堆dump顯示的某個目標對象A的實例一直在增加並且多了,一直沒回收,說明該對象引用的方法可能存在內存泄漏。
4)選中類目標對象A,右鍵選擇“在實例視圖中顯示”,如下所示:
左側是創建的實例總數,右側上部爲該實例的結構,下面爲引用說明,從圖中可以看出類A被誰引用了。
如此可以確定泄漏的位置,進而根據實際情況進行分析解決。

附:判斷對象是否被引用的工具或者方法?
----在我們內存泄漏問題排查的過程中就能看到對象是否被引用。通過jvisualvm導入堆dump文件,分析類標籤,下面能按類名過慮收索,收出來這個類之後,會看到後面有一個實例數=0.就是沒有被引用或不可達

案例3-線程死鎖、死循、請求外部資源導致的長時間wait---易引起:響應慢或無影響、長期會oom

一、 現象:-zabbix監控報警,接口一直超時,但服務端能收到,只是沒響應(裏面可能死鎖了),最近有發佈過一個新的功能。
二、分析問題,可能原因----集合HashMap放元素後業務異常了,死鎖了---生產環境版------使用jVisualvm分析內存泄漏,gceasy作輔助
1)jstack命令拿到對應的線程dump文件(按照程序輸出分開進行堆dump多次,好比較),同時下載GC日誌。(如果是本地或遠端測試環境可自已線程標籤生成dump文件)
2)先把GC日誌導入gceasy分析一下,發現老年代回收頻繁,但是內存並沒有明顯減少,說明存在無法被回收的可達對象,可能是內存泄漏了。(頻繁調用死鎖方法也可以產生大量存活對象)
3)把線程dump文件導入jVisualvm分析一下,導進來就看到各個線程的執行信息,分別比較一下兩個dump是否都有同樣的同題,比如:

非死鎖,但強等待的情況-------線程狀態BLOCKED,線程動作wait on monitor entry,調用修飾waiting to lock總是一起出現。表示在代碼級別已經存在衝突的調用。必然有問題的代碼,需要儘可能減少其發生。
死鎖-------dump寫的很明顯,直接告訴我們 Found one Java-level deadlock

 

-------------秒殺-性能測試-調優-------

再次就是看那些進程和和線程有問題,通過linux命令定位,然後jstack導出內存快照通過線程號找數據

對於大併發量的系統,有幾個可能需要優化的點,下面我們要一步步測試來優化這個系統。

測試目標

對於一個系統,幾個常用的評價指標是:平均響應時間、吞吐率、qps等。我的測試主要測試3個接口

主頁(訪問根路徑,沒有數據庫交互)
秒殺接口暴露(暴露秒殺接口,有後臺數據交互)
執行秒殺操作(插入秒殺成功記錄和減庫存一個完整的事務操作)
對於這三個接口,我們主要的測試目標和優化目標是平均響應時間,當然這是建立在數據正確返回的基礎上的,失敗率太高那這個平均響應時間是沒有意義的。
這裏的優化側重於後端數據庫和內存方面的優化。

測試環境

我是在Windows10下用jmeter來進行負載測試和壓力測試,其他環境如下,涉及具體配置再提。

Tomcat8.0.38
Jdk1.8 hotspot vm
Mysql 5.7
Redis 2.7.3

測試過程

首先進行主頁測試,我們訪問tomcat的主頁,使用jmeter的線程組中的線程數模擬用戶數,不斷增加線程數對主頁進行性能測試。
我們將結果數據寫到一個xml文件中。首先我們模擬5000個用戶同時請求主頁。

5000個用戶同時請求主頁

設置循環次數爲2,即一共有10000個請求將被髮送。

從響應的結果可以看到,沒有錯誤數,這10000個請求全部返回成功了,只是有的請求慢有的請求快。平均的響應時間在300ms, 50%的請求的響應時間平均爲87ms。到後面越來越多的請求開始等待,這裏可以想到的優化的點在於tomcat的線程池中線程的數量,越來越多的請求在等待隊列中。查看tomcat的配置後發現最大線程數爲maxThreads=”150”,好那我們用150個線程,循環10次,也就是一共1500個請求,那結果會是什麼樣呢?

平均相應時間爲5ms,前50%的請求的平均響應時間爲1ms。
但是這裏並不能直接修改tomcat的最大線程數來優化。複雜點說就是這是一個複雜的東西,線程數越大,你也要有相應的cpu來執行啊。直接點說就是,我不懂。。。
我把tomcat的線程數設置爲500,然後起5000個線程發送10000個請求,然後得到了:

 比之前的更差了。無論是平均相應時間還是錯誤率。簡單粗暴的去改線程數是不可行的。這裏我們不去管tomcat的線程數或者是其他層面的優化,我們只專注於後端數據庫層面的優化。

500個用戶同時請求暴露秒殺接口

爲什麼用500個,是爲了減少因爲tomcat請求等待帶來的數據誤差。 
直接向MySQL請求數據 
先模擬500個用戶,每個用戶發送10次請求。該請求相應的操作爲根據id向數據庫查詢一條記錄。得到了這樣的數據。

 期間打開windows的性能監控器,發現磁盤IO有變化,IO百分比最高的時候也不超過15%。 
這樣的操作,錯誤率爲0,相當穩定,平均響應時間爲1406ms。 

模擬5000個用戶,每個用戶發送一次請求

 磁盤的IO百分比一度達到了100%。從數據的絕對值來看,這樣的測試沒有意義了,因爲瓶頸不在MySQL,瓶頸是tomcat的連接池最大線程數爲maxThreads=”150” 越來越多的請求在等待隊列中,因爲我們前面分析過的tomcat。但是數據的相對值是有意義的。

使用redis緩存數據 

還是模擬500個用戶,每個用戶發送10次請求。

 響應速度顯著提高,注意一個值,Min=1,有些請求幾乎不足1ms,因爲redis直接從內存讀取數值,非常快。如果不是tomcat的請求在排隊,我想平均響應時間是個位數。 

Redis下模擬5000個用戶,每個用戶發送一次請求。會是什麼結果呢?

 可以看到:模擬5000個用戶比模擬500個用戶的響應時間要慢很多,平均響應時間大概是8倍

使用150個線程,循環100次,即發送15000次請求,得到:

 可以看到,150個用戶的話這種響應速度是比較快的,因此可以初步斷定:響應的瓶頸在於tomcat的請求排隊等待。

這個優化的過程我想到了很多東西,感覺就是,優化是無止盡的。
比如,我想到了內存回收那一塊。選用合適的垃圾收集器,儘可能地減少GC時stop the world的時間和次數顯然對於一個秒殺系統來說是非常對的優化方向。這裏我嘗試用過幾款垃圾收集器比如parNew,G1來對比他們的平均響應時間,但是多次測試後沒有明顯的差距。有兩個原因,一是這個接口沒有產生太多的大對象,二是這個優化並不太明顯。後面有機會的話還是希望繼續在內存方面進行優化,感覺內存回收方面有點神祕,很想試一試。

可以看到redis的使用很大程度上提高了響應的時間。上面那個接口只是暴露一個地址,這些地址每個產品都只有一個,那這樣的場景是可以用redis的。但是有些操作並沒有辦法使用緩存。比如執行秒殺這個操作。
這個操作是個事務型操作。如果其中一個操作失敗了,我就讓他rollback,這樣的話,應該會有更多的併發問題。
見下篇。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章