《沙盤模擬系列》JVM如何調優

紙上得來終覺淺 絕知此事要躬行html

我所在的公司基本上是沒有機會進行JVM參數調優的,可是若是有些東西本身不親身經歷一下,看再多的理論知識也只能算是紙上談兵,真正碰到問題的時候仍是不知道該怎麼分析。因此就本身製造一些問題而後看其現象,利用所學的知識事前推測,看現象是否是和本身推測的同樣。這樣不只對本身所學的知識又是一次鞏固,並且也能鍛鍊本身解決問題的能力(雖然問題是本身製造的)。java

其實在寫這篇文章以前已經看過好好幾遍關於JVM調優那一塊的內容,不管是書仍是博客,可是大都看完了感受本身懂了,可是真正本身模擬操做的時候又以爲什麼都不會,可是通過本身模擬一遍之後發現可以將以前的知識都關聯起來,造成了一個面,感受理解有深了一點。這裏強調一下但願你們看完之後,可以本身在機器上模擬一遍,採用不一樣的參數而後本身猜測結果並驗證git

工具準備

工欲善其事,必先利其器。在分析JVM以前咱們須要先將工具準備一下,一個是可視化的垃圾回收工具,另外一個是壓測的工具。github

GcViews安裝

  1. GcViews代碼從Git上下載下來github地址
  2. 在項目的根目錄中執行命令mvn clean install
  3. 而後發如今根目錄中生成了target文件夾,在裏面能夠找到gcviewer-1.37-SNAPSHOT.jar文件

JMeter安裝

Apache JMeter是一個開源的壓力測試具,JMeter是基於Java開發的,JMeter不只僅用於Web壓力測試,還用開源用於基於訪問式軟件作壓力測試,可對靜態文件、數據庫、FTP、SSH等作壓力測試算法

  • 下載JMeter,下載地址
  • 將其解壓下來,個人地址是/Users/hupengfei/apache-jmeter-5.1.1
  • 打開終端進入到其bin目錄下面
  • 執行命令sh jmeter

而後裏面如何配置參數的話我這裏就不細說了,你們能夠看這篇文章JMeter Http 壓力測試【圖解】數據庫

理論介紹

對於JVM調優來講,主要是對JVM垃圾收集的優化,通常來講是由於有問題了才須要優化,因此對於JVM的GC來講若是你觀察到你的應用服務進程的CPU使用率比較高,而且在GC日誌中發現GC次數比較頻繁、GC停頓時間長,這就代表你須要對GC進行優化了。apache

在對GC調優的過程當中,咱們不進行必要知道一些GC的原理,更重要要熟練使用各類可監控和分析的工具,具有GC調優的實戰能力。而目前來講使用率最高的兩款垃圾收集器有兩個一個是CMS一個是G1。從Java9開始,採用G1做爲默認的垃圾收集器,而G1的目標也是逐步要取代CMS。因此下面我簡單介紹一下這兩款收集器的區別。json

可使用命令java -XX:+PrintCommandLineFlags -version在命令行查看輸出默認的一些參數。此處可查看各個版本默認的垃圾收集器併發

  • Java 7: Parallel GC
  • Java 8: Parallel GC
  • Java 9: G1 GC
  • Java 10: G1 GC

CMS收集器

CMS收集器將Java堆分爲年輕代年老代(在Java8中就已經去掉了永久代,轉爲了元空間,而元空間是直接存儲在內存中的,並不在JVM中)。這主要是由於有研究代表,超過百分之90的對象在第一次GC時就會被回收掉,可是少數對象會存活較長的時間。app

CMS中還將年輕代分爲兩部分,一部分是倖存者空間(Survivor)伊甸園空間(Eden)。新的對象始終在Eden空間上建立,一旦一個對象在一次垃圾收集後還倖存的話,就會被移動到倖存者空間。當一個對象在屢次垃圾收集後還存活,它會被移動到年老代。這樣作的目的是在年輕代和年老代採用不一樣的垃圾收集算法,已達到較高的收集效率。好比因爲年輕代的對象存活時間較短,一次垃圾回收遺留的對象較少,因此採用複製-整理算法。可是在老年代中,對象存活時間較長,有可能一次垃圾回收回收的對象較少,遺留的對象較多,因此採用標記-整理算法

G1收集器

與CMS相比,G1有兩大特色

  • G1能夠併發完成大部分的GC工做,這期間不會「Stop-The-World」
  • G1使用非連續的空間,這使得G1可以有效的處理很是大的堆,G1能夠同時收集年輕代和老年代。G1並無將Java堆分紅三個空間(Eden、Survior和Old),而是將堆分紅了許多很是小的區域。這些區域的大小是固定的(默認狀況下每一個區域大小爲2MB)。每一個區域都分配一個空間。

圖中的U表示未分配的區域,G1將堆拆分紅小的區域,一個最大的好處就是可以作局部區域的垃圾回收,而不是每次要回收整個區域好比年輕代和年老代,這樣回收的停頓時間會比較短。收集過程大概以下

  • 將全部存活的對象從收集的區域複製到未分配的區域。好比收集的區域是Eden空間,把Eden中的存活對象複製到未分配的區域,這個未分配的區域就成了Survior空間,理想狀況下,若是一個區域所有是垃圾(意味一個存活的對象都沒有),則能夠直接將該區域聲明爲「未分配」。
  • 爲了優化收集時間,G1老是優先選擇垃圾最多的區域,從而最大限度減小後續分配和釋放堆空間所需的工做量。這也是G1收集器名字的由來——Garbage-First

實戰演練

我使用的版本是Java8,使用的Java垃圾回收器是CMS的

下面我經過實際的例子來實戰一下Java程序中因爲青年代設置太小,致使頻繁的GC,咱們將經過GC日誌分析工具來觀察GC活動並定位問題。

首先咱們創建一個SpringBoot的程序,做爲咱們的調優對象。代碼以下:

@RestController
@Slf4j
public class GcTestController {
    
    private List<Greeting> objListCache = new ArrayList<>();
    
    @RequestMapping("/greeting")
    public Greeting greeting() {
        Greeting greeting = new Greeting();
        if (objListCache.size() >= 100000) {
            log.info("clean the List!!!!!!!!!!");
            objListCache.clear();
        } else {
            objListCache.add(greeting);
        }
        return greeting;
    }
}

@Data
class Greeting {
    private String message1;
    private String message2;
    private String message3;
    private String message4;
    private String message5;
    private String message6;
    private String message7;
    private String message8;
    private String message9;
    private String message10;
    private String message11;
    private String message12;
    private String message13;
    private String message14;
    private String message15;
    private String message16;
    private String message17;
    private String message18;
    private String message19;
    private String message20;
}

上面代碼建立一個對象池,當對象池中的對象達到100000的時候纔會清空一次,用來模擬老年代的對象。這裏你們能夠利用我上一篇文章幾百萬數據放入內存不會把系統撐爆嗎?大概計算一下10W個對象放在內存中大概佔用多少內存。這裏我就直接說了10萬個Greeting 對象大概佔用10M的空間。

因此下面我在Idea中設置啓動參數設置,參數以下

-Xmx52m -Xmn9m -Xss256k -XX:+PrintGC -XX:+UseConcMarkSweepGC -Xloggc:/Users/hupengfei/Downloads/gclog/gc.log

我給程序設置的初始堆大小是52MB,設置的年輕代的大小爲9MB,年輕代中默認Eden區和Survior區比例是4:1,因此大概年輕代中Eden區大小爲7.2MB,目的是爲了讓你們看到在Eden區沒有回收的對象會進入到老年代,在Eden區滿了的話那麼就會發生Young GC。

而後咱們使用JMeter壓測工具向程序發送測試請求,注意這裏我設置的訪問時間是10分鐘,而後一個線程不間斷進行訪問。

十分鐘事後咱們可使用GCViewer工具打開GC日誌,咱們看到以下的這張圖

  • 藍色的線條:表示已經使用堆的大小,咱們看到它的週期是上下震盪的,這是由於咱們的對象池要擴展到10萬纔會被清空。
  • 底部綠色線條:表示發生GC活動,咱們能夠看到堆的使用率上升之後,會觸發頻繁的GC
  • 中間黑色的線條:表示Full GC,咱們能夠看到伴隨Full GC藍線降低了,這說明Full GC回收了老年代的對象

基於上面的圖所展示的,咱們能夠獲得一個結論,就是設置的年輕代不夠,爲何會得出這樣的結論呢?

  • GC活動頻繁:能夠看到綠色的線條比較密集
  • Java堆的內存在發生Full GC後可以被回收,說明不是內存泄露

經過GCView左邊的顯示,咱們能夠看到總GC發生了1622次其中Full GC發生一次。

接下來咱們在總堆大小不變的狀況下,咱們僅僅調整一下年輕代的大小,將其調整爲16MB,而後咱們再來看一下圖

咱們能夠看到雖然還有一次的Full GC 可是年輕代的GC並無那麼頻繁了。而且累計GC暫停的時間只有1.48秒

若是咱們還想繼續優化呢?就是繼續擴大堆內存的總大小,接下來咱們將堆設置爲200MB,年輕代設置爲80MB,咱們再來看一下效果。

能夠看到一樣時間內,已經沒有了Full GC,而且年輕代的GC發生更少了

調優策略

針對於CMS收集器來講,咱們要設置合理的年輕代和年老代的大小,你可能會問有沒有一個固定的公式呢?其實我這裏並無,調優的過程是一個迭代的過程,能夠採用JVM的默認值,而後進行壓測分析GC日誌。觀察在不一樣狀況下GC的回收狀況。

若是咱們看到頻繁發生Minor GC,而頻繁GC效率又不高,說明咱們的對象並無那麼快被回收,這時候咱們能夠適當調大年輕代大小,而後觀察。

若是咱們看到年老代的內存使用率處在高位,致使頻繁的發生Full GC。這種通常分爲兩種狀況

  • 若是每次Full GC年老代內存佔用率沒有下來,有多是內存泄漏,須要排查代碼
  • 若是Full GC後內存佔用率下來了,說明不是內存泄漏,能夠考慮調大老年代

代碼地址

已經將測試代碼放到了GitHub上https://github.com/modouxiansheng/Doraemon 上 而且將我屢次試驗的GC日誌也給放進去了,你們不想本身試驗的能夠將GC日誌給下載下來本身看一下圖

筆者文筆功力尚淺,若有不妥,請慷慨指出,一定感激涕零

總結

紙上的知識,或者說是書上或者網上的知識,終究仍是做者本身的經驗總結。必然有做者的思路。可是未必就與實際相結合,更重要的是一句話所要傳達的準確信息不是每一個人看過那種文字描述就能獲得的。若是偏偏有這方面的經歷就會產生共鳴。

我認爲,人讀書就是爲了學習,而學習也偏偏是爲了自身的成長。因此學習的中心在於人而不是書本。學習的本質就是在於要將本身所學的知識與自身相結合。若是不與自身相結合,本身不能對書本的知識產生共鳴,就很難深入理解書中的道理,天然也很難記住這種道理。

書中的知識,大可能是做者自身的理解和感悟,因此很難將這種讓做者共鳴的場景重現在讀者的腦海中,讓做者也產生共鳴,所以「紙上得來終覺淺」。只有解構書中的知識並與自身聯繫,「絕知此事要躬行」,那麼咱們在學習知識的同時也是在理解咱們自身,理解咱們所在的世界,得到心靈的共鳴,得到知識的鞏固。

因此我也會在我文章中再三的強調,若是你們想要對這方面知識更加深入的話,那麼必定要本身在機器上本身跑一遍,本身觀察一下,本身修改幾個參數,驗證一下狀況。有可能我碰到的坑你碰不到,你碰到的坑我沒碰到。那麼你碰到這個坑本身解決了就是對於本身能力的提高。

參考

相關文章
相關標籤/搜索