十五、JVM性能調優案例——性能優化


1、為什么要調優?

  • 防止出現OOM,進行JVM規划和預調優
  • 解決程序運行中各種OOM
  • 減少Full GC出現的頻率,解決運行慢、卡頓問題

2、調優的大方向

  • 合理地編寫代碼
  • 充分並合理的使用硬件資源
  • 合理地進行JVM調優

3、調優監控的依據

  • 運行日志異常堆棧
  • GC日志
  • 線程快照
  • 堆轉儲快照

4、性能優化的步驟

第1步:熟悉業務場景

第2步(發現問題):性能監控

  • GC頻繁
  • cpu lgad過高
  • OOM
  • 內存泄漏死鎖
  • 程序響應時間較長

第3步(排查問題):性能分析

  • 打印GC日志,通過GCviewer或者http://gceasy.io來分析日志信息
  • 靈活運用命令行工具,jstack, jmap, jinfo等
  • dump出堆文件,使用內存分析工具分析文件
  • 使用阿里Arthas,或jconsole,JVisualVM來實時查看JVM
  • jstack查看堆棧信息

第4步(解決問題):性能調優

  • 適當增加內存,根據業務背景選擇垃圾回收器
  • 優化代碼,控制內存使用
  • 增加機器,分散節點壓力
  • 合理設置線程池線程數量
  • 使用中間件提高程序效率,比如緩存,消息隊列等其他.......

  

一、調整堆大小提高服務的吞吐量

 

二、JVM優化之JIT優化

即時編譯對代碼的優化

1、逃逸分析

  • 如何將堆上的對象分配到棧,需要使用逃逸分析手段。
  • 逃逸分析(Escape Analysis)是目前Java虛擬機中比較前沿的優化技術。這是一種可以有效減少Java程序中同步負載和內存堆分配壓力的跨函數全局數據流分析算法
  • 通過逃逸分析,Java Hotspot編譯器能夠分析出一個新的對象的引用的使用范圍,從而決定是否要將這個對象分配到堆上
  • 逃逸分析的基本行為就是分析對象動態作用域:
    • 當一個對象在方法中被定義后,對象只在方法內部使用,則認為沒有發生逃逸。
    • 當一個對象在方法中被定義后,它被外部方法所引用,則認為發生逃逸。例如作為調用參數傳遞到其他地方中。
  • 沒有發生逃逸的對象,則可以分配到棧上,隨着方法執行的結束,棧空間就被移除
  • 逃逸分析包括:
    • 全局變量賦值逃逸
    • 方法返回值逃逸
    • 實例引用發生逃逸
    • 線程逃逸:賦值給類變量或可以在其他線程中訪問的實例變量

如何快速的判斷是否發生了逃逸分析,大家就看new的對象實體是否有可能在方法外被調用

2、代碼優化一:棧上分配

使用逃逸分析,編譯器可以對代碼做如下優化:

  • 棧上分配。將堆分配轉化為棧分配。如果經過逃逸分析后發現,一個對象並沒有逃逸出方法的話,那么就可能被優化成棧上分配。這樣就無需在堆上分配內存,也無須進行垃圾回收了。可以減少垃圾回收時間和次數
  • JIT編譯器在編譯期間根據逃逸分析的結果,發現如果一個對象並沒有逃逸出方法的話,就可能被優化成棧上分配。分配完成后,繼續在調用棧內執行,最后線程結束,棧空間被回收,局部變量對象也被回收。這樣就無須進行垃圾回收了。

3、代碼優化二:同步省略(消除)

同步省略。如果一個對象被發現只能從一個線程被訪問到,那么對於這個對象的操作可以不考慮同步:

  • 線程同步的代價是相當高的,同步的后果是降低並發性和性能。
  • 在動態編譯同步塊的時候,JIT編譯器可以借助逃逸分析來判斷同步塊所使用的鎖對象是否只能夠被一個線程訪問而沒有被發布到其他線程。如果沒有,那么JIT編譯器在編譯這個同步塊的時候就會取消對這部分代碼的同步。這樣就能大大提高並發性和性能。這個取消同步的過程就叫同步省略,也叫鎖消除

demo:

public class SynchronizedTest {
    public void f() {
        Object hollis = new Object();
        // 代碼中對hollis這個對象進行加鎖,但是hollis對象的生命周期只在f()方法中,並不會被其他線程所訪問到,所以在JIT編譯階段就會被優化掉。
        synchronized (hollis) {
            System.out.println(hollis);
        }
        /*
        JIT優化后:
        Object hollis = new Object();
         System.out.println(hollis);
         */
    }
}

4、代碼優化三:標量替換

  • 標量(Scalar)是指一個無法再分解成更小的數據的數據。Java中的原始數據類型就是標量。
  • 相對的,那些還可以分解的數據叫做聚合量(Aggregate) , Java中的對象就是聚合量,因為他可以分解成其他聚合量和標量。
  • 在JIT階段,如果經過逃逸分析,發現一個對象不會被外界訪問的話,那么經過JIT優化,就會把這個對象拆解成若干個其中包含的若干個成員變量來代替。這個過程就是標量替換

參數配置:-XX:+EliminateAllocations,開啟標量替換(默認打開),允許將對象打散分配在棧上。

demo

public class MyTestDemo {
    public static void main(String[] args) {
        alloc();
    }

    private static void alloc() {
        Point point = new Point(1, 2);
        System.out.println("point.x=" + point.x + "; point.y=" + point.y);
    }

    static class Point {
        private int x;
        private int y;

        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

    //alloc方法經過標量替換后,就會變成:
    private static void alloc1() {
        int x = 1;
        int y = 2;
        System.out.println("point.x=" + x + "; point.y=" + y);
    }
} 

5、逃逸分析小結

  • 逃逸分析無法保證非逃逸分析的性能消耗一定能高於他的消耗。雖然經過逃逸分析可以做標量替換、棧上分配、和鎖消除。但是逃逸分析自身也是需要進行一系列復雜的分析的,這其實也是一個相對耗時的過程
  • 一個極端的例子,就是經過逃逸分析之后,發現沒有一個對象是不逃逸的。那這個逃逸分析的過程就白白浪費掉了。
  • 雖然這項技術並不十分成熟,但是它也是即時編譯器優化技術中一個十分重要的手段
  • 注意到有一些觀點,認為通過逃逸分析,JVM會在棧上分配那些不會逃逸的對象,這在理論上是可行的,但是取決於JVM設計者的選擇。
  • 目前很多書籍還是基於JDK 7以前的版本,JDK已經發生了很大變化, intern字符串的緩存和靜態變量曾經都被分配在永久代上,而永久代已經被元數據區取代。但是,intern字符串緩存和靜態變量並不是被轉移到元數據區,而是直接在堆上分配,所以這一點同樣符合前面一點的結論:對象實例都是分配在堆上
  • JVM默認開啟逃逸分析,只要開啟逃逸分析,沒發生逃逸,就會棧上分配

 

三、合理配置堆內存

1、推薦配置

  • Java整個堆大小設置,Xmx 和 Xms設置為老年代存活對象的3-4倍,即FullGC之后的老年代內存占用的3-4倍
  • 永久代(jdk8為元數據MetaSpace)PermSize和MaxPermSize設置為FullGC之后老年代存活對象的1.2-1.5倍
  • 年輕代Xmn的設置為FullGC之后老年代存活對象的1-1.5倍。
  • 老年代的內存大小設置為FullGC之后老年代存活對象的2-3倍。

2、如何計算老年代存活對象

方式一(推薦):查看日志

開啟GC日志打印,例配置jvm參數 -XX:+PrintGC 觀察多次Full GC后老年代存活對象的大小取平均值為准

方式二(影響線上服務,慎用):強制觸發FullGC

方式1的方式比較可行,但需要更改JVM參數,並分析日志。同時,在使用CMS回收器的時候,有可能不能觸發FullGC(只發生CMS GC),所以日志中並沒有記錄FullGC的日志。在分析的時候就比較難處理。所以,有時候需要強制觸發一次FullGC,來觀察FullGC之后的老年代存活對象大小。

BTW:使用jstat -gcutil工具來看FullGC的時候, CMS GC是會造成2次的FullGC次數增加。

注:強制觸發FullGC,會造成線上服務停頓(STW)。建議的操作方式為,在強制FullCC前先把服務節點摘除,FullGC之后再將服務掛回可用節點,對外提供服務,在不同時間段觸發FullGC,根據多次FullGC之后的老年代內存情況來預估FullGC之后的老年代存活對象大小。

 

3、如何強制觸發Full GC?

  1. jmap -dump:live,format=b,file=heap.bin <pid>將當前的存活對象dump到文件,此時會觸發FullGC
  2. jmap -histo:live <pid>打印每個class的實例數目,內存占用,類全名信息..live子參數加上后,只統計活的對象數量,此時會觸發FullGC I
  3. 在性能測試環境,可以通過Java監控工具來觸發FullGC,比如使用VisualVM和JConsole,VisualVM集成了JConsole,VisualVM或者JConsole上面有一個觸發GC的按鈕。

4、案例演示

通過idea啟動springboot工程,我們將內存初始化為1024M,分析GC日志

  • -XX:+PrintGCDetails -XX:MetaspaceSize=64m -Xss512K
  • -XX:+HeapDumpOnOutOfMemoryError
  • -XX:HeapDumpPath=heap/heapdump3.hprof -XX:SurvivorRatio=8
  • -XX:+PrintGCDateStamps -Xms1024M -Xmx1024M
  • -Xloggc:log/gc-oom3.log

5、估算GC頻率

正常情況我們應該根據我們的系統來進行一個內存的估算,這個我們可以在測試環境進行測試,最開始可以將內存設置的大一些,比如4G這樣,當然這也可以根據業務系統估算來的。

比如從數據庫獲取一條數據占用128個字節,需要獲取1000條數據,那么一次讀取到內存的大小就是((128 B/1024 Kb/1024M)* 1000 = 0.122M,那么我們程序可能需要並發讀取,比如每秒讀取100次,那么內存占用就是0.122*100 = 12.2M,如果堆內存設置1個G,那么年輕代大小大約就是333M,那么333M*80% / 12.2M =21.84s ,也就是說我們的程序幾乎每分鍾進行兩到三次youngGC。這樣可以讓我們對系統有一個大致的估算。

 

四、CPU占用很高排查方案

問題分析

  1. ps aux | grep java查看到當前java進程使用cpu、內存、磁盤的情況獲取使用量異常的進程
  2. top -Hp進程pid檢查當前使用異常線程的pid
  3. 線程pid變為16進制如31695- 》 7bcf然后得到0x7bcf6、jstack進程的pid | grep -A20 Ox7bcf得到相關進程的代碼

 

五、G1並發執行的線程數對性能的影響

參數配置

export CATALINA_OPTS="$CATALINA_OPTS -XX :+UseG1GC"

export CATALINA_OPTS="$CATALINA_OPTS -Xms30m"

export CATALINA_OPTS="$CATALINA_OPTS -Xmx30m"

export CATALINA_OPTS="$CATALINA_OPTS -XX :+PrintGCDetails"

export CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=64m"

export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDateStamps"

export CATALINA_OPTS="$CATALINA_OPTS -Xloggc:/opt/tomcat8.5/logs/gc.log"

export CATALINA_OPTS="$CATALINA_OPTS -XX:ConcGCThreads=1"

說明:最后一個參數可以在使用G1GC測試初始並發GCThreads之后再加上。
初始化內存和最大內存調整小一些,目的發生 FullGC,關注GC時間
關注點是:GC次數,GC時間,以及 Jmeter的平均響應時間

 

增加線程配置:
export CATALINA_OPTS="$CATALINA_OPTS -XX:ConcGCThreads=2",可以提高系統的吞吐量

 

六、調整垃圾回收器提高服務的吞吐量

初始配置

系統配置是單核,我們看到日志,顯示DefNew,說明我們用的是串行收集器,SerialGC

優化1

那么就考慮切換一下並行收集器是否可以提高性能,增加配置如下:

export CATALINA_OPTS="$CATALINA_OPTS -Xms60m"

export CATALINA_OPTS="$CATALINA_OPTS -Xmx60m"

export CATALINA_OPTS="$CATALINA_OPTS -xX:+UseParallelGC"

export CATALINA_OPTS="$CATALINA_OPTS -xx:+PrintGCDetails"

export CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=64m"

export CATALINA_OPTS="$CATALINA_OPTS -XX :+PrintGCDateStamps"

export CATALINA_OPTS="$CATALINA_OPTS -Xloggc :/opt/tomcat8.5/logs/gc6.log"

查看吞吐量,發現並沒有明顯變化,我們究其原因,本身UseParallelGC是並行收集器,但是我們的服務器是單核

優化2

系統配置改為8核,並使用優化1的參數配置,發現吞吐量大幅提升,說明我們在多核機器上面采用並行收集器對於系統的吞吐量有一個顯著的效果。

優化3

8核+G1垃圾收集器

export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC"

export CATALINA_OPTS="$CATALINA_OPTS -Xms60m"

export CATALINA_OPTS="$CATALINA_OPTS -Xmx60m"

export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDetails"

export CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=64m"

export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDateStamps"

export CATALINA_OPTS="$CATALINA_OPTS -Xloggc:/opt/tomcat8.5/logs/gc6.log"

相對ParallelGC,使用G1吞吐量大幅提升

 

七、日均百萬級訂單交易系統如何設置JVM參數

八、問題一:系統卡頓、響應慢

有一個50萬PV的資料類網站(從磁盤提取文檔到內存)原服務器是32位的,1.5G的堆,用戶反饋網站比較緩慢。因此公司決定升級,新的服務器為64位,16G的堆內存,結果用戶反饋卡頓十分嚴重,反而比以前效率更低了。

1.為什么原網站慢?

頻繁的GC,STW時間比較長,響應時間慢

2.為什么會更卡頓?

內存空間越大,FGC時間更長,延遲時間更長

3.咋辦?

  • 垃圾回收器:parallel GC;ParNew + CMS;G1
  • 配置GC參數:-XX:MaxGCPauseMillis 、-XX:ConcGCThreads
  • 根據log日志、dump文件分析,優化內存空間的比例
    • jstat jinfo jItack jmap

 

九、問題二:系統內存飆高,如何查找問題?

一方面:jmap -heap . jstat . ...; gc日志情況
另一方面:dump文件分析

 

十、問題三:如何監控JVM?

jps,查看正在運行的Java進程

jstat,查看JVM統計信息

jinfo,實時查看和修改JVM配置參數

jmap,導出內存映像文件&內存使用情況

jhat,JDK自帶堆分析工具

jstack,打印JVM中線程快照

jcmd,多功能命令行

jstatd,遠程主機信息收集

 


免責聲明!

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



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