天天看點

JVM性能優化

JVM 性能優化。

一、記憶體溢出

記憶體溢出的原因:程式在申請記憶體時,沒有足夠的空間。

1. 棧溢出

方法死循環遞歸調用(***Error)、不斷建立線程(OutOfMemoryError)。

2. 堆溢出

不斷建立對象,配置設定對象大于最大堆的大小(OutOfMemoryError)。

3. 直接記憶體

JVM 配置設定的本地直接記憶體大小大于 JVM 的限制,可以通過-XX:MaxDirectMemorySize 來設定(不設定的話預設與堆記憶體最大值一樣,也會出現OOM 異常)。

4. 方法區溢出

一個類要被垃圾收集器回收掉,判定條件是比較苛刻的,在經常動态生産大量 Class 的應用中,CGLIb 位元組碼增強,動态語言,大量 JSP(JSP 第一次運作需要編譯成 Java 類),基于 OSGi 的應用(同一個類,被不同的加載器加載也會設為不同的類),都可能會導緻OOM。

二、記憶體洩露

程式在申請記憶體後,無法釋放已申請的記憶體空間,導緻這一部分的原因主要是代碼寫的不合理,比如以下幾種情況。

1. 長生命周期的對象持有短生命周期對象的引用

例如将 ArrayList 設定為靜态變量,然後不斷地向ArrayList中添加對象,則 ArrayList 容器中的對象在程式結束之前将不能被釋放,進而造成記憶體洩漏。

2. 連接配接未關閉

如資料庫連接配接、網絡連接配接和 IO 連接配接等,隻有連接配接被關閉後,垃圾回收器才會回收對應的對象。

3. 變量作用域不合理

例如:

  • 一個變量的定義的作用範圍大于其使用範圍。
  • 如果沒有及時地把對象設定為 null。

4. 内部類持有外部類

Java 的 非靜态内部類 的這種建立方式,會隐式地持有外部類的引用,而且預設情況下這個引用是強引用,是以,如果内部類的生命周期長于外部類的生命周期,程式很容易就産生記憶體洩露(可以了解為:垃圾回收器會回收掉外部類的執行個體,但由于内部類持有外部類的引用,導緻垃圾回收器不能正常工作)。

解決辦法:将非靜态内部類改為 靜态内部類,即加上 static 修飾,例如:

public class Jvm5 {
    private static String string = "SuunyBear";

    public static void show() {
        System.out.println("show");
    }

    public static void main(String[] args) {
        Jvm5 m = new Jvm5();
        // 非靜态内部類的構造方式
        // Child c=m.new Child();
        Child c = new Child();
        c.test();
    }

    /**
     * 内部類Child --靜态的,防止記憶體洩漏
     */
    static class Child {
        public int i;

        public void test() {
            System.out.println("string:" + string);
            show();
        }
    }
}
           

5. Hash值改變

在集合中,如果修改了對象中的那些參與計算哈希值的字段,會導緻無法從集合中單獨删除目前對象,造成記憶體洩露。

使用例子來說明。

public class Jvm6 {
    private int x;
    private int y;

    public Jvm6(int x, int y) {
        super();
        this.x = x;
        this.y = y;
    }
    /**
     * 重寫HashCode的方法
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + x;
        result = prime * result + y;
        return result;
    }
    /**
     * 改變y的值:同時改變hashcode
     */
    public void setY(int y) {
        this.y = y;
    }

    public static void main(String[] args) {
        HashSet<Jvm6> hashSet = new HashSet<Jvm6>();
        Jvm6 data1 = new Jvm6(1, 3);
        Jvm6 data2 = new Jvm6(3, 5);
        hashSet.add(data1);
        hashSet.add(data2);
        data2.setY(7); // data2的Hash值改變
        hashSet.remove(data2); // 删掉data2節點
        System.out.println(hashSet.size()); // 2
    }
}
           

三、記憶體溢出和記憶體洩漏辨析

  • 記憶體溢出:實實在在的記憶體空間不足導緻。
  • 記憶體洩漏:該釋放的對象沒有釋放,常見于使用容器儲存元素的情況下。

如何避免:

  • 記憶體溢出:檢查代碼以及設定足夠的空間。
  • 記憶體洩漏:一定是代碼有問題,往往很多情況下,記憶體溢出往往是記憶體洩漏造成的。

四、了解MAT

mat是一個記憶體洩露的分析工具。

1. 淺堆和深堆

  • 淺堆(Shallow Heap):是指一個對象所消耗的記憶體。
  • 深堆(Retained Heap):這個對象被 GC 回收後,可以真實釋放的記憶體大小,也就是隻能通過對象被直接或間接通路到的所有對象的集合。通俗地說,就是一個對象包含(引用)的所有對象的大小,如圖:
JVM性能優化

2. MAT的使用

1、下載下傳MAT工具:下載下傳位址

2、記憶體溢出例子示範

參數說明:

  • -Xms5m 堆初始大小5M
  • -Xmx5m 堆最大大小5M
  • -XX:+PrintGCDetails 列印gc日志詳情
  • -XX:+HeapDumpOnOutOfMemoryError 輸出記憶體溢出檔案
  • -XX:HeapDumpPath=D:/oomDump/dump.hprof 記憶體溢出檔案儲存位置,此檔案用于MAT分析
/**
 * VM Args:-Xms5m -Xmx5m  -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/oomDump/dump.hprof
 */
public class Jvm7 {

    public static void main(String[] args) {
        // 在方法執行的過程中,它是GCRoots
        List<Object> list = new LinkedList<>();
        int i = 0;
        while (true) {
            i++;
            if (i % 10000 == 0) {
                System.out.println("i=" + i);
            }
            list.add(new Object());
        }
    }
}
           

設定參數運作後,記憶體溢出,程式結束,然後我們就可以用下載下傳好的MAT來分析了,當然MAT也隻是分析猜想,并不代表一定是這個原因導緻記憶體溢出。

打開我們儲存的檔案目錄進行分析。

JVM性能優化

分析結果。

JVM性能優化

此時可以檢視詳情檢視具體原因,當然這個原因也隻是一種猜想。

五、JDK提供的一些工具

分類 屬性值 描述
指令行工具 jps 虛拟機程序狀況工具
jstat 虛拟機統計資訊監視工具
jinfo Java配置資訊工具
jmap Java記憶體映像工具
jhat 虛拟機堆轉儲快照分析工具
jstack Java堆棧跟蹤工具
可視化工具 JConsole Java監視與管理控制台
VisualVM 多合一故障處理工具

所有的工具都在jdk的安裝bin目錄下,比如我的在

C:\My Program Files\Java\jdk1.8.0_201\bin

其中一般情況指令行線上上伺服器上使用,可視化工具在本地使用,當然如果你的線上伺服器允許遠端的話也可以使用可視化工具。

六、GC調優

1. GC調優重要參數

生産環境推薦開啟

  • -XX:+HeapDumpOnOutOfMemoryError
    • 輸出記憶體溢出檔案
  • -XX:HeapDumpPath=D:/oomDump/dump.hprof
    • 記憶體溢出檔案儲存位置,此檔案用于MAT分析
    • 當然,一般Linux伺服器可以設定為

      ./java_pid<pid>.hprof

      預設為Java程序啟動位置

調優之前開始,調優之後關閉

  • -XX:+PrintGC
    • 調試跟蹤之 列印簡單的 GC 資訊參數:
  • -XX:+PrintGCDetails和-XX:+PrintGCTimeStamps
    • 列印詳細的 GC 資訊
  • -Xlogger:logpath:log/gc.log
    • 設定 gc 的日志路,将 gc.log 的路徑設定到目前目錄的 log 目錄下. 應用場景: 将 gc 的日志獨立寫入日志檔案,将 GC 日志與系統業務日志進行了分離,友善開發人員進行追蹤分析

考慮使用

  • -XX:+PrintHeapAtGC
    • 列印推資訊,擷取 Heap 在每次垃圾回收前後的使用狀況
  • -XX:+TraceClassLoading
    • 在系統控制台資訊中看到 class 加載的過程和具體的 class 資訊,可用以分析類的加載順序以及是否可進行精簡操作
  • -XX:+DisableExplicitGC
    • 禁止在運作期顯式地調用 System.gc()

2. GC調優的原則(很重要)

  • 大多數的 java 應用不需要 GC 調優
  • 大部分需要 GC 調優的的,不是參數問題,是代碼問題
  • 在實際使用中,分析 GC 情況優化代碼 比 優化 GC 參數 要多得多
  • GC 調優是最後的手段

調優的目的

  • GC 的時間夠小
  • GC 的次數夠少發生
  • Full GC 的周期足夠的長,時間合理,最好是不發生

注: 如果滿足下面的名額,則一般不需要進行 GC調優

  • Minor GC 執行時間不到 50ms
  • Minor GC 執行不頻繁,約 10 秒一次
  • Full GC 執行時間不到 1s
  • Full GC 執行頻率不算頻繁,不低于 10 分鐘 1 次

3. GC調優步驟

1、監控 GC 的狀态使用各種 JVM 工具,檢視目前日志,分析目前 JVM 參數設定,并且分析目前堆記憶體快照和 gc 日志,根據實際的各區域記憶體劃分和 GC 執行時間,覺得是否進行優化。

2、分析結果,判斷是否需要優化如果各項參數設定合理。

  • 系統沒有逾時日志出現,GC 頻率不高,GC 耗時不高,那麼沒有必要進行 GC 優化。
  • 如果 GC 時間超過 1 秒,或者頻繁 GC,則必須優化。

3、調整 GC 類型和記憶體配置設定如果記憶體配置設定過大或過小,或者采用的 GC 收集器比較慢,則應該優先調整這些參數,并且先找 1 台或幾台機器進行 測試,然後比較優化過的機器和沒有優化的機器的性能對比,并有針對性的做出最後選擇。

4、不斷的分析和調整通過不斷的試驗和試錯,分析并找到最合适的參數5,全面應用參數如果找到了最合适的參數,則将這些參數應用到所有伺服器,并進行後續跟蹤。

分析GC日志

主要關注 MinorGC 和 FullGC 的回收效率(回收前大小和回收比較)、回收的時間。

1、-XX:+UseSerialGC

  • 以參數-Xms5m -Xmx5m -XX:+PrintGCDetails -XX:+UseSerialGC 為例詳細說明。
  • [DefNew: 1855K->1855K(1856K), 0.0000148 secs][Tenured: 2815K->4095K(4096K), 0.0134819 secs] 4671K。
  • DefNew 指明了收集器類型,而且說明了收集發生在新生代。
  • 1855K->1855K(1856K)表示,回收前 新生代占用 1855K,回收後占用 1855K,新生代大小 1856K
  • 0.0000148 secs 表明新生代回收耗時。
  • Tenured 表明收集發生在老年代。
  • 2815K->4095K(4096K), 0.0134819 secs:含義同新生代最後的 4671K 指明堆的大小。

2、-XX:+UseParNewGC

  • 收集器參數變為-XX:+UseParNewGC。
  • 日志變為:[ParNew: 1856K->1856K(1856K), 0.0000107 secs][Tenured: 2890K->4095K(4096K), 0.0121148 secs]。
  • 收集器參數變為-XX:+ UseParallelGC 或 UseParallelOldGC。
  • 日志變為:[PSYoungGen: 1024K->1022K(1536K)] [ParOldGen: 3783K->3782K(4096K)] 4807K->4804K(5632K)。

3、-XX:+UseConcMarkSweepGC 和 -XX:+UseG1GC

使用這兩個收集器的日志會和UseParNewGC一樣有明顯的相關字樣。

4. 項目啟動調優

開啟日志分析-XX:+PrintGCDetails,啟動項目時,通過分析日志,不斷地調整參數,減少GC次數。

例如:

1、碰到 Metadata空間 不足發生GC,那麼調整 Metadata空間

-XX:MetaspaceSize=64m

減少 FullGC 。

2、碰到MinorGC,那麼調整堆空間

-Xms1000m

大小減少FullGC 。

3、如果還是有MinorGC,那麼繼續增大堆空間大小,或者增大新生代比例

-Xmn900m GC

,此時新生代空間為900m,老年代大小100m 。

5. 項目運作GC調優

使用 jmeter 工具 來進行壓測,然後分析原因,進行調優,當然 正式上線的項目請謹慎操作 。

jmeter工具安裝使用

1、下載下傳好對應版本的jmeter,注意jdk版本。

JVM性能優化

2、jmeter需要Java運作時環境,是以如果報錯請先檢查你的Java環境變量設定,解壓到你想要的路徑,例如我解壓在

C:\My Program Files\apache-jmeter-5.2.1

,在bin目錄下有一個

jmeter.bat

檔案,輕按兩下啟動。

至于具體怎麼使用就百度吧,基本拿到軟體就知道使用了,畢竟這個說來就浪費篇幅了。

聚合報告參數

這裡放出我本地 jmeter 測試一個項目之後的 聚合報告參數解釋。

JVM性能優化

6. 推薦政策(僅作參考)

1、新生代大小選擇

  • 盡可能設大,直到接近系統的最低響應時間限制(根據實際情況選擇).在此種情況下,新生代收集發生的頻率也是最小的.同時,減少到達老年代的對象。
  • 避免設定過小,當新生代設定過小時會導緻:MinorGC 次數更加頻繁、可能導緻 MinorGC 對象直接進入老年代,如果此時老年代滿了,會觸發 FullGC。

2、老年代大小選擇

一般吞吐量優先的應用都有一個很大的新生代和一個較小的老年代.原因是,這樣可以盡可能回收掉大部分短期對象,減少中期的對象,而老年代盡存放長期存活對象

七、逃逸分析

補充知識,并非所有的對象都會在堆上面配置設定,而沒有在堆上配置設定的對象是因為經過逃逸分析,分析之後發現該對象的大小可以在棧上配置設定,不會造成棧溢出,這時,對象就可以在棧上配置設定。

當然,如果經過逃逸分析,發現該對象在棧上配置設定會照成棧溢出,那麼該對象就會在堆空間配置設定。

參數jdk1.8預設開啟

  • -XX:+DoEscapeAnalysis 啟用逃逸分析(預設打開)
  • -XX:+EliminateAllocations 标量替換(預設打開)
  • -XX:+UseTLAB 本地線程配置設定緩沖(預設打開)

八、常用的性能評價/測試名額

一個 web 應用不是一個孤立的個體,它是一個系統的部分,系統中的每一部分都會影響整個系統的性能。

1、響應時間:送出請求和傳回該請求的響應之間使用的時間,一般比較關注平均響應時間。

2、并發數:同一時刻,對伺服器有實際互動的請求數,和網站線上使用者數的關聯:1000 個同時線上使用者數,可以估計并發數在 5%到 15%之間,也就是同時并發數在 50~150 之間。

3、吞吐量:對機關時間内完成的工作量(請求)的量度,例如1秒處理5萬個請求。

都讀到這裡了,來個 點贊、評論、關注、收藏 吧!

文章作者:IT王小二

首發位址:https://www.itwxe.com/posts/5878703e/

版權聲明:文章内容遵循 署名-非商業性使用-禁止演繹 4.0 國際 進行許可,轉載請在文章頁面明顯位置給出作者與原文連結。