1. 程式人生 > >JVM垃圾回收機制演算法分析

JVM垃圾回收機制演算法分析

JVM記憶體執行時資料區


一、什麼是垃圾回收機制

gc垃圾回收機制&&演算法

什麼是垃圾回收機制:

不定時去堆記憶體清理不可達物件。不可達的物件並不會馬上就會直接回收,而是至少要經過兩次標記的過程。

public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        // test = null;
        System.gc(); // 手動回收垃圾
    }

    @Override
    protected void 
finalize() throws Throwable { // gc回收垃圾之前呼叫 System.out.println("垃圾回收機制..."); } }
演算法:
標記清除
引用計數法
複製演算法
標記壓縮
分代演算法

二、記憶體溢位與記憶體洩露的區別

記憶體溢位:專案需要4G記憶體,伺服器只有3G記憶體,記憶體不夠用

記憶體洩漏的定義:物件已經沒有被應用程式使用,但是垃圾回收器沒辦法移除它們,因為還被引用著。

定義很多靜態變數,垃圾是不會回收的,這個物件又沒有被引用,再申請記憶體時報錯記憶體洩漏。

三、引用計數演算法


概述:給物件中新增一個引用計數器,每當一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器都為0時的物件就是不再被使用的,垃圾收集器將回收該物件使用的記憶體。  用在eden區。

優缺點:
優點:引用計數收集器可以很快的執行,交織在程式執行中。
缺點:無法檢測出迴圈引用。如父物件有一個子物件的引用,子物件反過來引用父物件,這樣他們的引用計數永遠不可能為0,而且每次加減非常浪費記憶體。

引用計數,每個物件都會有一個標記,預設是15次,gc回收時,為0時 gc直接回收掉。不為0時,物件沒有被引用減1,被引用加1。如果次數加到大於15,進入到新生代s0區,或者s1區(複製演算法),再增加次數比較多的時候進入老年代(分代演算法)。

四、複製演算法

複製演算法--新生代(s0、s1大小是相等的,只有一個區域能存活) 


複製演算法優點:連續性,不會產生碎片化(碎片化表示殘留)。


五、標記清除與標記壓縮演算法

標記清除和標記壓縮一般用在老年代。



六、分代收集演算法 

       根據記憶體中物件的存活週期不同,將記憶體劃分為幾塊,Java的虛擬機器中一般把記憶體劃分為新生代和老年代,當新建立物件時一般在新生代中分配記憶體空間,當新生代垃圾連續回收幾次之後仍然存活的物件會被移動到老年代記憶體中,當大物件在新生代中無法找到足夠的連續記憶體時也直接在老年代中建立。

對於新生代和老年代來說,新生代回收頻率很高,但是每次回收耗時很短,而老年代回收頻率較低,但是耗時會相對較長,所以應該儘量減少老年代GC。

為什麼垃圾回收機制如果頻繁執行會降低程式效率? 垃圾回收時的停頓現象

       垃圾回收的任務是識別和回收垃圾物件進行記憶體清理,為了讓垃圾回收器可以更高效的執行,大部分情況下,會要求系統進入一個停頓的狀態。停頓的目的是為了暫停所有的應用執行緒,只有這樣的系統才不會有新垃圾的產生。同時停頓保證了系統狀態在某一個瞬間的一致性,也有利於更好的標記垃圾物件。因此在垃圾回收時,都會產生應用程式的停頓。

新生代:eden區  採用引用計數演算法; s0 s1區 採用複製演算法。
老年代:採用標記壓縮演算法,一般不會用標記清除演算法,會產生碎片。

七、垃圾收集器與jmeter壓力測試工具用法

什麼是垃圾收集器?
序列收集器 單執行緒收集垃圾 效率低
單執行緒執行回收操作,回收期間暫停所有應用執行緒的執行,client模式下的預設回收器,通過-XX:+UseSerialGC命令列可選項強制指定。一般不用序列收集器。

並行收集器 多執行緒收集垃圾 效率高  調優選擇並行

並行回收器
並行回收器在序列回收器基礎上做了改進,他可以使用多個執行緒同時進行垃圾回收,對於計算能力強的計算機而言,可以有效的縮短垃圾回收所需的實際時間。
並行回收器工作時的執行緒數量可以使用XX:ParallelGCThreads引數指定。

Tomcat配置調優測試---Jmeter壓力測試工具 

先新增執行緒組
新增sampler http請求

新增監聽器 聚合報告


Average:平均響應時間
Median:中位數,也就是50%使用者的響應時間
90%Line:90%使用者的響應時間
Min:最小響應時間
Max:最大響應時間
Error%:本次測試中出現的錯誤的請求的數量/請求的總數
Throughput:吞吐量--預設情況下表示每秒完成的請求數

KB/Sec:每秒從伺服器端接收到的資料量

八、tomcat引數調優測試-序列回收

測試序列吞吐量
-XX:+PrintGCDetails -Xmx32M -Xms32M
-XX:+HeapDumpOnOutOfMemoryError
-XX:+UseSerialGC
-XX:PermSize=32M

擴大堆的記憶體
-XX:+PrintGCDetails -Xmx512M -Xms32M

結論:最大記憶體越大,吞吐量越高

調整初始堆記憶體

-XX:+PrintGCDetails -Xmx512M -Xms512M

結果:吞吐量提高

Edit Configuration



九、tomcat引數調優測試-並行回收

並行回收(UseParallelGC)
-XX:+PrintGCDetails -Xmx512M -Xms512M
-XX:+HeapDumpOnOutOfMemoryError
-XX:-UseParallelGC

-XX:PermSize=32M

新生代回收UseParNewGC在jdk9中被移除...

並行合併回收(UseParallelGC)
-XX:+PrintGCDetails -Xmx512M -Xms512M
-XX:+HeapDumpOnOutOfMemoryError
-XX:+UseParallelOldGC
-XX:ParallelGCThreads=8

-XX:PermSize=32M

ParallelGCThreads:設定垃圾收集器並行階段使用的執行緒數,一般設定是cpu核數*2


結論:並行回收吞吐量高於序列回收

jvm引數官方文件地址:http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html

gc調優指南:http://www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html


深入理解Java虛擬機器與引數調優

一、Java記憶體結構概述


java記憶體模型和Java記憶體結構區別:
java記憶體模型(多執行緒JVM)
java記憶體結構(jvm虛擬機器儲存空間)

jvm記憶體結構



二、新生代與老年代 



三、堆記憶體引數配置

虛擬機器引數配置

什麼是虛擬機器引數配置

       在虛擬機器執行的過程中,如果可以跟蹤系統的執行狀態,那麼對於問題的故障排查會有一定的幫助,為此,在虛擬機器提供了一些跟蹤系統狀態的引數,使用給定的引數執行Java虛擬機器,就可以在系統執行時列印相關日誌,用於分析實際問題。我們進行虛擬機器引數配置,其實就是圍繞著堆、棧、方法區進行配置。

堆的引數配置
-XX:+PrintGC                  每次出發GC的時候列印相關日誌
-XX:+UseSerialGC           序列回收
-XX:+PrintGCDetails       更詳細的GC日誌
-Xms 堆初始值
-Xmx 堆最大可用值
-Xmn 新生代堆最大可用值

-XX:SurvivorRatio            用來設定新生代中eden空間和from/to空間的比例

總結:在實際工作中,我們可以直接將初始的堆大小與最大堆大小相等,這樣的好處是可以減少程式執行時垃圾回收次數,從而提高效率。

常用api

import java.text.DecimalFormat;

/**
 * Created by yz on 2018/03/26.
 */
public class JvmDemo {
    public static void main(String[] args) throws InterruptedException {
        byte[] bytes01 = new byte[1*1024*1024];
        System.out.println("分配了1M記憶體");
        jvmInfo();
        Thread.sleep(3000);
        byte[] bytes02 = new byte[4*1024*1024];
        System.out.println("分配了4M記憶體");
        jvmInfo();
    }

    /**
     * 將kb轉換成M
     * @param maxMemory
     * @return
     */
    private static String toM(long maxMemory){
        float num = maxMemory/(1024*1024);
        DecimalFormat df = new DecimalFormat("0.00");
        String m = df.format(num);
        return m;
    }

    /**
     * 檢視最大記憶體配置資訊、當前空閒記憶體、已使用記憶體
     */
    private static void jvmInfo(){
        // 最大記憶體配置資訊 預設的堆記憶體大小
        long maxMemory = Runtime.getRuntime().maxMemory();
        System.out.println("最大記憶體配置資訊:"+maxMemory+","+toM(maxMemory)+"M");
        // 當前空閒記憶體
        long freeMemory = Runtime.getRuntime().freeMemory();
        System.out.println("當前空閒記憶體:"+freeMemory+","+toM(freeMemory)+"M");
        // 已使用記憶體
        long totalMemory = Runtime.getRuntime().totalMemory();
        System.out.println("已使用記憶體:"+totalMemory+","+toM(totalMemory)+"M");
    }
}
設定最大堆記憶體

引數:-Xms5M -Xmx20M -Xlog:gc* -XX:+UseSerialGC -XX:+PrintCommandLineFlags


初始值5M 最大值20M  垃圾回收機制回收了5次 

[3.455s][info][gc             ] GC(4) Pause Young (Allocation Failure) 5M->4M(9M) 5.649ms

修改值為:-Xms20M -Xmx20M

垃圾回收機制回收了2次

[3.398s][info][gc           ] GC(1) Pause Young (Allocation Failure) 5M->4M(19M) 3.026ms

jvm引數調優:

1.堆初始值與堆記憶體最大值一定要保持一致,減少垃圾回收機制次數。

為什麼堆初始值越比堆記憶體最大值越小,垃圾回收機制次數越多?

因為一上來想用20M,但是初始值只有5M,這個時候要去回收,不然沒有空間。生產環境初始值20M 最大值4G 等於沒有調優。調優目的,就是減少垃圾回收次數,每次垃圾回收時是非常影響效率的。

修改值為:-Xms1024M -Xmx1024M

這次沒有垃圾回收機制再回收。生成環境預設值為4G,根據伺服器cup配置。


四、配置新生代與老年代調優引數

jvm引數調優:

2.設定新生代與老年代回收比例。

設定新生代與老年代優化引數
-Xmn 新生代大小,一般設為整個堆的1/3到1/4左右
-XX:SurvivorRatio 設定新生代中eden區和from/to空間比例關係 n/1

引數:-Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2 -Xlog:gc* -XX:+UseSerialGC

-XX:SurvivorRatio=2 表示eden區是from區/to區的兩倍,這個引數配的不多,主要配新生代與老年代的關係。

-Xmn1m 表示設定新生代記憶體大小為1m

/**
 * Created by yz on 2018/03/26.
 */
public class JvmDemo {
    /**
     * 設定新生代與老年代優化引數
     * @param args
     */
    public static void main(String[] args){
        // -Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2 -Xlog:gc* -XX:+UseSerialGC
        byte[] bytes = null;
        for (int i = 0; i < 10; i++) {
            System.out.println("i:"+i);
            bytes = new byte[1*1024*1024];
        }
    }
}
[0.370s][info][gc,heap,exit ]    eden space 512K,  57% used [0x00000000fec00000, 0x00000000fec49448, 0x00000000fec80000)
[0.370s][info][gc,heap,exit ]    from space 256K,  16% used [0x00000000fec80000, 0x00000000fec8aaf8, 0x00000000fecc0000)

[0.370s][info][gc,heap,exit ]   to   space 256K,   0% used [0x00000000fecc0000, 0x00000000fecc0000, 0x00000000fed00000)


設定新生代與老年代分代關係的引數 

-Xms20m -Xmx20m -XX:NewRatio=2 -XX:SurvivorRatio=2 -Xlog:gc* -XX:+UseSerialGC

-XX:NewRatio=2 表示老年代是新生代的2倍 新生代與老年代比例 1/3或者1/4 目的是讓垃圾回收機制儘量去新生代裡去回收,儘量減少老年代進行回收。

/**
 * Created by yz on 2018/03/26.
 */
public class JvmDemo {
    /**
     * 設定新生代與老年代優化引數
     * @param args
     */
    public static void main(String[] args){
        // -Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2 -Xlog:gc* -XX:+UseSerialGC
        // -Xms20m -Xmx20m -XX:NewRatio=2 -XX:SurvivorRatio=2 -Xlog:gc* -XX:+UseSerialGC  使用這個引數
        byte[] bytes = null;
        for (int i = 0; i < 10; i++) {
            System.out.println("i:"+i);
            bytes = new byte[1*1024*1024];
        }
    }
}
[0.345s][info][gc,heap,exit ]    eden space 3456K,  92% used [0x00000000fec00000, 0x00000000fef1f7d0, 0x00000000fef60000)
[0.345s][info][gc,heap,exit ]    from space 1664K,  61% used [0x00000000fef60000, 0x00000000ff060120, 0x00000000ff100000)
[0.345s][info][gc,heap,exit ]    to   space 1664K,   0% used [0x00000000ff100000, 0x00000000ff100000, 0x00000000ff2a0000)
[0.345s][info][gc,heap,exit ]  tenured generation   total 13696K, used 2885K [0x00000000ff2a0000, 0x0000000100000000, 0x0000000100000000)

[0.345s][info][gc,heap,exit ]    the space 13696K,  21% used [0x00000000ff2a0000, 0x00000000ff5716f8, 0x00000000ff571800, 0x0000000100000000)


五、堆溢位解決辦法 

設定堆記憶體大小 -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError

-XX:+HeapDumpOnOutOfMemoryError 記憶體溢位提示報錯資訊

錯誤原因:java.lang.OutOfMemoryError: Java heap space 堆記憶體溢位

/**
 * Created by yz on 2018/03/26.
 */
public class JvmDemo {
    /**
     * 堆溢位解決辦法 以下程式碼需要多少m堆記憶體
     * @param args
     */
    public static void main(String[] args){
        List<Object> list = new ArrayList<Object>();
        for (int i = 0; i < 10; i++) {
            System.out.println("i:"+i);
            list.add(new byte[1*1024*1024]);
        }
        System.out.println("建立完畢!");
    }
}

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

解決辦法:將最大堆記憶體引數設大些 -Xmx40m

伺服器端開發出現此錯誤時(Tomcat記憶體溢位),修改Tomcat catalina.sh檔案 設定jvm堆記憶體大小
JAVA_OPTS="-server -Xms800m -Xmx800m -XX:PermSize=256m -XX:MaxPermSize=512m -XX:MaxNewSize=512m"


六、棧溢位解決辦法

什麼是棧溢位?就是無限的遞迴引用

棧溢位是在操作一個變數,方法在無限遞迴呼叫的時候,就會丟擲棧溢位異常 java.lang.StackOverflowError

/**
 * Created by yz on 2018/03/26.
 */
public class JvmDemo {

    private static int count = 0;

    public static void getCount(){
        try {
            count++;
            getCount();
        } catch (Throwable e) {
            System.out.println("最大的深度..."+count);
            e.printStackTrace();
        }
    }

    public static void main(String[] args){
        getCount();
    }
}

解決辦法:設定執行緒最大呼叫深度

-Xss5m

設定前

最大的深度...11859
java.lang.StackOverflowError

設定後:

最大的深度...243126

java.lang.StackOverflowError

注意:

棧溢位產生於遞迴呼叫,迴圈遍歷是不會的,但是迴圈方法裡面產生遞迴呼叫,也會發送棧溢位。

/**
 * Created by yz on 2018/03/26.
 */
public class JvmDemo {

    private static int count = 0;

    public static void getCount(){
        try {
            count++;
            //getCount();
            System.out.println(count);
        } catch (Throwable e) {
            System.out.println("最大的深度..."+count);
            e.printStackTrace();
        }
    }

    // 棧溢位是方法中遞迴呼叫,不是迴圈呼叫方法發生棧溢位。這裡不會發生棧溢位
    public static void main(String[] args){
        for (int i = 0; i < 1000; i++) {
            getCount();
        }
    }
}