文章目錄
JVM - 聊聊調優那些事,調優我們需要注意啥?
大家一起學到這一章,其實對
JVM
就已經有了不錯的認識並且可能已經產生了自己的見解。這篇文章我主要的目的是想以一種輕鬆的方式和大家分享一下我在這段時間學習JVM
相關知識後自己的一些理解和看法,有問題希望大家指出,一起探討交流。
1.傳統項目和互聯網項目有什麼區別?
其實我們大家平時工作的項目主要就分爲傳統項目和互聯網項目,但是無論是哪一種其實最後都可能會存在JVM的問題。這裏我們可以稍稍做一個比較來給大家分享下我的一些看法。
首先是傳統項目,我提幾點我的理解,大多數接觸過傳統項目的小夥伴應該也有所感受。
用戶羣體比較固定
:傳統IT項目主要通過和某些其他單位或者個人有合作,會針對性地開發出一套系統以在特定範圍內解決特定領域的問題。以用戶爲中心的思想較弱
:由於用戶羣體比較固定,比如開發某套系統僅供某客戶公司內部員工使用,這也就導致了系統就算不好但也不得不用。傳統IT項目更多時候以合作客戶爲中心而不是以使用用戶爲中心。需求被動且明確
:只要合作客戶有需要,就會盡力把需求明確並開發。並且一旦需求明確之後,會儘可能按照嚴格要求開發,嚴格控制需求變更,需求上線前會經過多次測試驗證,上線後則儘量不變動。
這裏針對以上幾點我說一下我對互聯網項目的理解。
用戶羣體不固定
:互聯網項目的目的主要是解決我們每一個身處互聯網範圍覆蓋內用戶的生活需求。比如我們公司主要開發互聯網電視相關的業務,讓每個使用我們公司產品的用戶更快更好地去獲取更優質的互聯網影視資源,有可能在項目初期我們由於某種影視資源傾斜等各種原因主要面向少兒羣體,但由於公司的發展策略會各種縱橫橫向擴展業務線,從而整個產品面向的用戶羣體就會不斷擴散。以用戶爲中心
:互聯網項目核心目的就是提供優質的服務,所以用戶體驗一定要好才能夠吸引更多的用戶,否則用戶就會流失到其他同類產品中去。需求主動且迭代快
:互聯網產品更多時候需要公司更主動更積極去挖掘用戶新的需求開發新的功能提供新的體驗。由於同類產品競爭大所以某項新功能從立項到上線時間很短,需要更快投入到市場上第一時間吸入用戶。並且由於互聯網產品的特性,其他公司產品也很快會上線同類型功能,所以需要通過快速迭代和競爭對手拉開差距。
2.項目如何進行調優?
2.1 項目可能出現的常見問題?
小夥伴們都知道傳統IT項目中接觸比較頻繁的就是上傳下載。各種業務數據、員工數據、表單爲了方便操作都會開發一個上傳下載的功能。但是當操作的文件數據過大時,只要沒考慮周全、處理得當,其實是很容易發生各種問題的,另外一個就是服務單點故障。
而互聯網項目中比較常見的就是服務接口性能瓶頸、多線程異常,內存溢出等問題。
其實不管是哪一種項目,在排除了各個層面異常之後,都是可以通過JVM的調優對其進行一定程度的優化的。
2.2 回顧MionorGC和FullGC
下面我會通過幾個簡單的示例來介紹如何對項目進行JVM相關的調優從而達到一個更理想的效果。在此之前,我們先來回顧一下
MionorGC
和FullGC
。因爲在很多情況下,JVM相關的問題都離不開GC的關係。
Minor GC
:當Eden區域不足分配時就會觸發。
Full GC
:
- 調用
System.gc()
會建議JVM進行Full GC
,雖然真正是否執行由JVM決定,但在很大程度上還是會觸發,並且強行執行可能會影響到GC正常執行,所以建議儘量少使用。當然我們也可以通過-XX:+ DisableExplicitGC
參數來禁止該方法的調用。- 老年代空間不足時也會觸發
Full GC
。之前我們知道了大對象會直接進入老年代,並且長期存活的對象也會進入老年代,若Full GC
後老年代空間仍不足則會拋出OutOfMemoryError
。- 空間分配擔保失敗時會觸發
Full GC
。之前我們有介紹過使用複製算法在進行Minor GC
階段需要使用老年代的部分空間進行擔保,如果擔保後仍然分配失敗則會觸發一次Full GC
。
2.3 GC調優(示例1)
關於JVM調優,可以通過選擇合適的垃圾收集器、合適的垃圾算法,這裏我們主要介紹如何通過JVM參數對項目進行調優。
JVM調優其實主要就是調整兩個指標:
停頓時間
:GC中斷應用執行的時間。吞吐量
:除去GC時間佔總時間比例,例如GC時間1/(1+n)
則吞吐量爲n/(1+n)
。
2.3.1 輸出GC日誌
要想對項目進行調優,就需要有依據。這裏我隨便找了一個之前的項目,在啓動時加上VM參數
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:E://gc.log
,這行參數主要就是將GC
產生日誌記錄到一個文件中。
2.3.2 分析GC日誌(GC Easy)
我們找到輸出的GC日誌,通過日誌我們可以很明顯分析出這次
Full GC
是由於元空間不足導致的。
之前給大家介紹過一款PerfMa
分析工具,那款工具主要針對堆和線程快照進行分析。這裏再給大家安利一款【GC Easy】,這款工具可以針對我們的GC LOG
進行分析,更直觀看到各項指標,如果大家願意掏一點小錢的話他甚至還會給你一點意見,還是挺好用的。這裏我就用免費版來帶大家瞭解下這款工具。
選中我們的GC日誌點擊分析,稍等一小會。
整個分析過程還是挺快的,當然我這個GC文件也不大。這裏我們可以看到有一個Recommendations
,這個如果你是付費用戶的話他就會根據GC日誌分析的結果給出一定的建議,雖然建議還挺中肯,不過要針對特定業務的話還是需要自己去稍加分析。
首先我們看到這個JVM memory size
,這裏給我們統計出了各代中內存分配
及其峯值
大小,還繪製了一張圖更直觀就能夠進行查看和比較。
Key Performance Indicators
這個對於我們調優來說是一個比較重要的東西,是一個關鍵指標。主要用來統計應用程序在執行過程中的一個吞吐量
、GC平均停頓時間
和GC最大停頓時間
,這幾個指標都是我們系統性能評判的一個標準,大家可以着重關注下。
Interactive Graphs
是一個關係圖譜,描述了包括Heap GC前後使用情況
、每次GC時長
以及新生代/老年代 GC前後分配情況
等。
GC Statistics
是用來統計所有GC情況記錄的,可以很清楚地看到每種GC總共發生了多少次、回收了多少空間、消耗了多少時間等各種GC相關信息。
GC Causes
幫我們列舉了我們應用GC造成的原因。例如這裏告訴我們應用有6次元空間觸碰到了閾值以及6次分配時間,還能夠看到每種原因所花費的各種時間以及佔比。
當我們GC日誌過大時,通過GC Easy
其實對於我們分析整個系統還是更加高效地。大家可以
去導入一份GC日誌嘗試使用GC Easy
去分析感受一下,不過對原生日誌的分析技能我們也不能丟掉噢。
2.3.3 分析GC原因,調整參數進行調優
我們通過上一步,得出了由於元空間分配不足引起了多次分配失敗導致觸發了
Full GC
。我們還通過GC Easy
看到了我們應用的各項指標,很容易就確定了是由於元空間不足造成的GC。
這裏我們對JVM參數稍作調整-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:MetaspaceSize=128M -XX:MaxMetaspaceSize=128M -Xloggc:E://gc1.log
,修改一下元空間的大小再來看一看情況怎麼樣。
首先我們來看一下頭部,和之前比較各個空間的內存分配都有了一定程度的下降,尤其是元空間。另外細心的小夥伴會發現Recommendations
木有了,這說明通過對這次的GC日誌進行分析並沒有發現有不符合常理的情況。
我們再來看看關鍵指標,這裏可以看到我們的吞吐量已經從85%
提升到了97.5%
,而且GC停頓時間也大幅下降了。
拉到GC統計可以看到Full GC
已經沒有發生了,那麼這就表明我們通過分析以及對JVM參數的調整,對整個應用程序起到了一個不錯的調優效果。大家也可以試着找一些性能發生瓶頸的項目去自己進行分析調優,這樣可以讓大家對GC的調優有更深刻的認識。
2.4 GC調優(示例2)
2.4.1 分析原因
這裏我給大家再簡單描述另一個場景,大家一起來跟着思考該如何去優化?
假設我們有一個會員服務部署了集羣,每個節點是4核8G的機器。現在有一個會員訂單的業務,假設每秒會產生300個訂單,以會員訂單對象爲例假設每個訂單對象大小1KB,那麼每秒就會產生300KB的訂單對象。
由於我們訂單業務裏面還涉及了庫存、優惠券、積分、權益等各種其他相關對象,我們將數據量放大10倍(300KB x 10)。另外除了這些對象,我們還可能包括庫存查詢、優惠券計算、積分統計修改、權益匹配、會員訂單數據同步推送等各種關聯業務,將數據量再次放大20倍(3000KB x 20),此時我們每秒會產生60M的一個數據量。
大家注意這裏我們的各種放大隻是因爲我們後面需要達到這個數值,並非真實過程中就是這樣的,大家不需要太注重這個數據量怎麼膨脹的。
這裏先給大家看下我們啓動時的JVM參數:-Xms3072M -Xmx3072M -XX:MaxMetaspaceSize=512M -XX:MetaspaceSize=512M
。這樣看好像並沒有什麼問題,但是我們程序跑一段時間後發現越來越慢,當我們把GC日誌拉下來後發現總是隔一會就進行Full GC
,這是怎麼回事呢?
爲了更直觀,我這裏簡單地畫出了我們這個應用的堆分配情況方便大家理解。
我們開始說了我們的程序每秒會產生大概60M的對象,那麼我們也知道對象是會優先在Eden
區域進行分配。這裏Eden
區800M的話大概我們每隔800/60≈13s
就會觸發一次Minor GC
。
大家思考一下,其實當我們觸發Minor GC
時,如果我們這裏以秒爲單位去衡量的話,那麼除了最後一秒產生的對象依然存活,之前的所有對象其實都已經無效了。那麼Minor GC
就會將最後一秒產生的60M對象放到S0區,然後將Eden區其他對象進行回收,如果僅僅是如此那麼可能還沒有關係,但是我們之前知道一個對象要進入到老年代有幾種情況:大對象直接分配在老年代
、Survivor區中年齡達到閾值對象進入老年代
、相同年齡對象大於Survivor空間一半直接進入老年代
。
這裏60M對象就這樣順理成章進入老年代了,那麼2G的老年代夠我們玩多久呢?大概夠我們來33次,那麼33 x 13大概7分鐘就會佔滿一次老年代觸發一次Full GC
。也難怪我們的程序總是會變慢了。
2.4.2 對症下藥
通過上面我們已經知道了當服務運行時間長了會變慢的原因,那麼我們來看看怎麼調優。
我們適當調整一下VM參數:-Xms3072M -Xmx3072M -Xmn2048M -XX:MaxMetaspaceSize=512M -XX:MetaspaceSize=512M
,這裏我們將新生代大小設置爲了2G,堆中的一個空間佔比如下圖。
我們再來看看我們的服務會怎樣去運行,是否達到了我們所要的效果呢?
同樣的每秒產生60M對象,每26s發生了一次Minor GC
,此時最後一秒產生的60M對象進入S0不會進入老年代,並且Eden
區中對象會被回收。
下一次流程也就是26s後Eden
區再次滿了觸發Minor GC
,此時最後一秒產生的60M對象會分配到S1區,並會將Eden
和另一塊非空的SurvivorS0
區進行垃圾回收。這個流程反覆執行會發現,我們業務所進行的對象分配由於其性質,會完全在年輕代中分配和回收不會進入老年代,所以Full GC
也就不復存在了。
3.JVM優化需要注意啥?
上面我們通過兩個小示例來對GC調優有了一個簡單的認識。我們如果要避免上面原因引起的應用問題,其實就是儘量去規避
Full GC
,需要做的就是儘可能地讓對象在Minor GC
階段被回收。讓對象在新生代中多存活一段時間並且儘量避免創建過大的對象和數組。
我們要熟練地對JVM進行調優,就一定要對對象在內存中的分配方式和回收策略熟悉。下面介紹一些我們在JVM調優時的通用思路策略。
3.1 讓對象留在年輕代
我們都知道
Full GC
的成本遠高於Minor GC
,我們的服務經常出現"卡"其實就很可能是因爲應用在頻繁進行Full GC
。
JVM會優先在Eden
區分配對象,但若由於空間分配不足觸發Minor GC
後會將存活的對象往Survivor
區轉移。倘若Survivor
區也不足以分配或對象所需空間佔總量超過50%,那麼這批對象會直接放到老年代,久而久之就和我們上面示例一樣,隔一段時間觸發一次Full GC
。
我們可以通過設置一個合理的Eden
區和Survivor
區,或者調整相應的空間比例儘可能去將對象留在年輕代中被回收掉,避免更多的對象進入老年代,從而可以有效地避免頻繁Full GC
。使用最多的VM參數就是-Xmn
去調整年輕代的大小。
3.2 讓合適的大對象進入老年代
上面我們說了我們需要如果將對象留在年輕代,但是有一種情況是我們需要注意的,那就是大對象。不知道大家是否還記得在【JVM - 內功修煉之內存分配與回收策略】中,我給大家講過一個大對象直接分配在老年代的情況,那麼JVM會無緣無故弄出這樣一個策略嗎?
答案當然是不會的。由於大對象佔用內存較大,如果都直接分配在年輕代中,那麼有可能會擾亂年輕代的內存分配頻繁觸發Minor GC
。由於大對象佔用空間很大,所以需要將大量小的對象從Eden
區移到老年代,這些對象很快就會消亡,又會導致頻繁Full GC
。
所以我們可以通過XX:PretenureSizeThreshold=3145728(3MB)
這種方式設置一個閾值,當分配對象大於這個數值時則會直接分配在老年代中。這裏設置的3M只是一個示例,需要大家根據實際情況進行判斷和設置才能達到最理想的效果。
另外一個需要注意的是,如果我們使用的這些大對象又大多是生命週期很短的對象,那對於GC也是十分致命的。老年代本身是我們用來存放生存週期比較長對象的一塊空間,若全被短命對象佔領其實對整個GC的設計和運行機制都有影響。所以我們在設計和編寫代碼的過程中是需要儘量去避免使用短命的大對象的。
3.3 設置進入老年代的年齡
一般情況下,對象會被分配在
Eden
區中。當進行了一次Minor GC
後,對象依然存活,則會移至Survivor
區中,並且會爲其維護的年齡加1。當經歷過很多次Minor GC
後對象依然堅挺,年齡達到了一定的數值時,則會被移至老年代中。
我們可以通過XX:MaxTenuringThreshold
設置進入老年代最大年齡的閾值,實際上是否進入老年代的年齡還需要根據JVM在運行時動態計算的,這個設置只是確定一個最大邊界。如果我們想讓對象多留在年輕代一會,可以設置一個比較大的閾值,但這對於性能的好處也不是絕對的。
3.4 設置穩定的Java堆
我們在項目中經常會看到有很多人會將
-Xmx
和-Xms
設置成相同的。這是因爲當最大堆和初始堆大小設置成一樣的情況下我們可以擁有一個穩定的堆,穩定的堆由於不會動態擴容在一定程度上可以減少GC的次數。
但是我們可以思考一下,穩定的堆由於大小固定爲最大值。雖然減少了GC次數,但是每次回收的堆空間都是固定那麼大,所以會增加每次GC所需的時間。
我們還可以通過-XX:MinHeapFreeRatio
和-XX:MaxHeapFreeRatio
參數調整堆空間最大/最小空閒比例,大家有興趣可以去了解一下,不過如果對自己應用和JVM不熟悉的話還是不要一頓操作爲好。
這裏列舉出來的情況大多是平時遇見比較多的一些調優方式,還有很多其他的方式大家可以通過其他博主博客或者網上文檔之類的瞭解嘗試,調優這種東西還是很靠經驗的。
4.最後一些話
首先還是十分感謝大家能夠認真看完我寫的博客,這其中有一些我也是通過書籍、其他博客學習然後自己嘗試總結,最後結合自己工作中的經驗和自己的理解整理出來分享給大家的。從工作年限來說的話,我還是很稚嫩的,還有很多要跟大家一起學習一起進步的地方。
這章給大家介紹了一些我對JVM調優的理解,對於調優大家主要就是熟練掌握之前JVM的相關知識,然後善於利用類似VisualVM
、GC Easy
這類工具提高查找問題效率,久而久之擁有豐富經驗的你也會成爲那個調優大牛的。
到這裏整個JVM基本也告一段落了。在這裏和大家一起學習了JVM的整個內存結構、對象分配與回收策略、各種垃圾算法、各種垃圾收集器,我們還了解了JIT技術
及逃逸分析,到最後我們掌握瞭如何使用虛擬機工具武裝我們自己,然後就是我們這裏的JVM調優了。這裏會把這個過程中我使用到的代碼和一些相關文件以及整理一份腦圖附在第一篇文章【JVM - 進入Java虛擬機的真實世界】裏,希望能夠對大家有幫助,最後再次感謝大家的支持和包容!