JVM 性能優化的經典案例

源 /Java1234         文 / IT 王小二

一、內存溢出

內存溢出的原因:程序在申請內存時,沒有足夠的空間。

1. 棧溢出

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

2. 堆溢出

不斷創建對象,分配對象大於最大堆的大小(OutOfMemoryError)。

3. 直接內存

JVM 分配的本地直接內存大小大於 JVM 的限制,可以通過 - XX:MaxDirectMemorySize 來設置(不設置的話默認與堆內存最大值一樣, 也會出現 OOM 異常)。

4. 方法區溢出

一個類要被垃圾收集器回收掉,判定條件是比較苛刻的,在經常動態生產大量 Class 的應用中,CGLIb 字節碼增強,動態語言,大量 JSP(JSP 第一次運行需要編譯成 Java 類), 基於 OSGi 的應用 (同一個類,被不同的加載器加載也會設爲不同的類),都可能會導致 OOM。

二、內存泄露

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

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

例如將 ArrayList 設置爲靜態變量,然後不斷地向 ArrayList 中添加對象,則 ArrayList 容器中的對象在程序結束之前將不能被釋放,從而造成內存泄漏。

2. 連接未關閉

如數據庫連接、網絡連接和 IO 連接等,只有連接被關閉後,垃圾回收器纔會回收對應的對象。

3. 變量作用域不合理

例如:

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. 淺堆和深堆

2. MAT 的使用

1、下載 MAT 工具:下載地址

2、內存溢出例子演示

參數說明:

/**
 * 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 也只是分析猜想,並不代表一定是這個原因導致內存溢出。

打開我們保存的文件目錄進行分析。

分析結果。

此時可以查看詳情查看具體原因,當然這個原因也只是一種猜想。

五、JDK 提供的一些工具

pHTqkq

所有的工具都在 jdk 的安裝 bin 目錄下,比如我的在C:\My Program Files\Java\jdk1.8.0_201\bin

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

六、GC 調優

1. GC 調優重要參數

生產環境推薦開啓

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

考慮使用

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

調優的目的

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

3. GC 調優步驟

1、監控 GC 的狀態使用各種 JVM 工具,查看當前日誌,分析當前 JVM 參數設置,並且分析當前堆內存快照和 gc 日誌,根據實際的各區域內存劃分和 GC 執行時間,覺得是否進行優化。

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

3、調整 GC 類型和內存分配如果內存分配過大或過小,或者採用的 GC 收集器比較慢,則應該優先調整這些參數,並且先找 1 臺或幾臺機器進行 測試,然後比較優化過的機器和沒有優化的機器的性能對比,並有針對性的做出最後選擇。

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

分析 GC 日誌

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

1、-XX:+UseSerialGC

2、-XX:+UseParNewGC

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 版本。

2、jmeter 需要 Java 運行時環境,所以如果報錯請先檢查你的 Java 環境變量設置,解壓到你想要的路徑,例如我解壓在C:\My Program Files\apache-jmeter-5.2.1,在 bin 目錄下有一個 jmeter.bat 文件,雙擊啓動。

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

聚合報告參數

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

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

1、新生代大小選擇

2、老年代大小選擇

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

七、逃逸分析

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

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

參數 jdk1.8 默認開啓

八、常用的性能評價 / 測試指標

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

1、響應時間:提交請求和返回該請求的響應之間使用的時間,一般比較關注平均響應時間。
2、併發數:同一時刻,對服務器有實際交互的請求數,和網站在線用戶數的關聯:1000 個同時在線用戶數,可以估計併發數在 5% 到 15% 之間,也就是同時併發數在 50~150 之間。
3、吞吐量:對單位時間內完成的工作量 (請求) 的量度,例如 1 秒處理 5 萬個請求。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Z4lmiFigVroqmgSjmrrAWA