jvm線上調優實戰


  在前面文章我們了解到了jvm的內存模型、對象分配的規則、以及對象何時進入到老年代、垃圾回收器,並且知道jvm調優的本質就是對堆內存進行調優,盡量使對象留在新生代中、少觸發老年代gc。那么本文將介紹生產環境上如何去排查問題這樣的一個思路。用的是最原始、有low、也最有效的jstat命令,因為每個公司情況不一樣你不一定有權限使用jConsole、VisualVM那些可視化工具。

如何監控進程jvm信息

  我們通過 jstat -gc PID 命令就可以來監控java進程的內存和GC情況了。

S0C:第一個幸存區的總大小
S1C:第二個幸存區的總大小
S0U:第一個幸存區當前已使用大小
S1U:第二個幸存區當前已使用大小
EC: 伊甸園區的總大小
EU: 伊甸園區的使用大小
OC:老年代總大小
OU:老年代當前已使用大小
MC:方法區(元空間)總大小
MU:方法區(元空間)當前已使用大小
CCSC:壓縮類空間大小
CCSU:壓縮類空間使用大小
YGC:系統迄今為止年輕代垃圾回收次數
YGCT:年輕代垃圾回收總消耗時間和
FGC:系統迄今為止老年代垃圾回收次數
FGCT:老年代垃圾回收總消耗時間和
GCT:所有gc消耗總時間

除了 jstat -gc PID 之外,還有一些其他命令可以看到詳細信息了,不過單獨這一個命令就已經夠用了。
jstat -gccapacity 堆內存分析
jstat -gcnew 年輕代GC分析,TT和MTT表示年輕代對象最小年齡和最大年齡
jstat -gcnewcapacity 年輕代內存分析
jstat -gcold 老年代GC分析
jstat -gcoldcapacity 老年代內存分析
jstat -gcmetacapacity 元空間內存分析

新生代對象增長速率

   jstat -gc PID time num ,這個命令就是每隔指定time時間監控一次jvm信息,一共統計num次。比如我們現在每隔1s統計一次,一共統計5次。

   我們現在只需要看“EU”這一列,就可以看到eden區的使用情況了。如果第一次是200mb,第二次205mb,第三次209mb。那么我們可以估算出每秒eden區新增5mb對象。我們可以根據自己系統的情況設置成每分鍾或者每十分鍾監控一次,也可以看看系統高峰期和日常兩種情況下對象的增長速率。

Young GC觸發頻率和耗時

   我們知道eden區新增對象的速率后,就可以計算出young gc觸發的頻率和耗時了。比如eden區有800mb對象,每秒新增5m,那么160s就會把eden區裝滿、差不多3分鍾的樣子就會觸發一次young gc。如果每秒新增0.5m對象,那就是30分鍾一次young gc。gc耗時我們可以用 ygct/ygc 求出每次young gc的所用時長。

老年代對象增長速率

3分鍾一次young gc,那我們就3分鍾監控一次jvm信息,查看10次。jstat -gc PID 180000 10

   此時可以看看young gc后 eden survivor 老年代 的對象變化。正常來講,eden區每次放滿對象再gc后,里面的對象會變得很少。然后survivor區會放入一些存活對象,老年代也會增長一些對象。

  正常來講,一般老年代對象不會增長的很快,因為我們的系統其實沒那么多長期存活的對象。如果每次young gc后,老年代增長幾十mb,說明young gc后存活的對象太多了需要調優下,一般新增 幾百kb、幾mb才是正常水准。我們通過這十次每次進入老年的的對象大小求出平均數,就知道老年代的增長速率啦。

Full GC觸發頻率和耗時

   比如現在我們知道每3分鍾一次young gc后有50mb進入老年代,那么48分鍾就會把老年代裝滿觸發一次full gc。比如現在一共進行了10次full gc共耗時20s,那么每次耗時就是2s。

jvm調優實戰

案例代碼

/**

java  -jar -Xmn100M -Xms200M -Xmx200M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=20M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log  demo-1.0.jar

 新生代100m , 總堆200m , eden:from:to=8:1:1 , 超過15歲進入老年代 , 大對象閾值20m , parnew+cms回收
 */
public class Demo1 {

    public static void main(String[] args) throws Exception{
        // 系統先休眠30s,給我們足夠的時間找到pid來監測進程情況。
        Thread.sleep(30000);
        while (true){
            loadData();
        }
    }

    private static void loadData()throws Exception{
        byte[] data = null;
        // 每次生成40mb垃圾對象
        for(int i=0;i<4;i++){
            data = new byte[10 * 1024 * 1024];
        }
        data = null;
        // data1 data2 被引用不會成為垃圾
        byte[] data1 = new byte[10 * 1024 *1024];
        byte[] data2 = new byte[10 * 1024 *1024];
        byte[] data3 = new byte[10 * 1024 *1024];
        // data3指向新對象,之前的成為垃圾
        data3 = new byte[10 * 1024 *1024];

        // 阻塞1s,來模擬每個請求執行完需要1s
        Thread.sleep(1000);
    }
}

gc分析

1. 最開始eden區一直只有1638kb,說明線程還在30s的阻塞中沒開始創建對象
2. 下一秒可以看到已經觸發了young gc了。eden區只有80m,我們創建了80m對象再加上一些系統自己占用的內存,所以eden就放不下了。回收了282k在survivor區中(一些系統未知對象),data1 data2 data3 一共30mb放入了老年代。
3. 每次young gc都會存活很多對象,而survivor放不下,都會直接進入老年代
4. 老年代從30m、50m、60m又變成了30m,說明老年代放不下了對60m進行了fgc回收,裝了新的30m

  通過對比可以看到每秒執行一次ygc,每2秒左右執行一次fgc。每次ygc差不多22/0.146=0.0067秒,每次fgc差不多0.013/10=0.0013秒。ygc執行時間是fgc5倍。所以不難發現,我們的ygc不是由於老年代自己空間不足發生的,而是新生代ygc的時候survivor放不下,觸發了老年代的空間擔保機制和動態年齡判斷造成的。ygc每次都要等fgc將老年代清出空余位置后才把對象放入老年代。所以才會出現ygc比fgc時間要長的情況。

性能調優

  鑒於上一步的分析,我們可以看出由於新生代的內存不足,導致每次ygc都會將對象放入老年代,頻繁觸發fgc。所以我們要加大新生代內存空間,最好survivor區可以放下每次ygc的對象。

java  -jar -Xmn200M -Xms300M -Xmx300M -XX:SurvivorRatio=2 -XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=20M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log  demo-1.0.jar


新生代200m,總堆300m,eden:s0:s1=2:1:1    此時查看jvm可以看到沒有觸發fgc了,只有少量對象進入老年代中。一般的系統young gc幾分鍾或幾十分鍾一次,每次不超過幾十毫秒是正常的;full gc幾十分鍾或幾小時一次,幾百毫秒之內是正常的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM