【擁抱大廠系列】百度面試官問過的 “JVM記憶體分配與回收策略原理”,我用這篇文章搞定了

歐陽思海發表於2020-04-29

在前面的一篇文章深入理解Java虛擬機器-如何利用VisualVM進行效能分析中講到了一些關於JVM調優的知識,但是,其實,還是有一些問題沒有非常清楚的可以回答的,這裡先給出幾個問題,然後,我們再展開這篇文章需要講解的知識。

  • 我們生成的物件最開始在哪分配?Eden?Survivor?還是老年代呢?
  • 進入到老年代需要滿足什麼條件呢?

接下來,我們就帶著這兩個問題展開全文。

1 物件優先在哪分配

其實,通過前面幾篇文章的講解,這個問題其實已經見怪不怪了,在大多數的情況下,物件都是在新生代Eden區分配的,在前面的文章我們提到,在Eden區中如果記憶體不夠分配的話,就會進行一次Minor GC。同時,我們還知道年輕代中預設下Eden:Survivor0:Survivor2 = 8:1:1,同時,還能通過引數-XX:SurvivorRatio來設定這個比例(關於這些引數的分析都可以檢視這篇文章:深入理解Java虛擬機器-常用vm引數分析)。

下面我們通過一個例子來分析是不是這樣的。

1.1 例項

給定JVM引數:-Xms40M -Xmx40M -Xmn10M -verbose:gc -XX:+PrintGCDetails -XX:SurvivorRatio=4

前面三個引數設定Java堆的大小為40M,新生代為10M,緊跟著後面兩個是用於輸入GC資訊。更多引數可以檢視這篇文章:深入理解Java虛擬機器-常用vm引數分析

/**
 * @ClassName Test_01
 * @Description 引數:-Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails -XX:+UseSerialGC -XX:SurvivorRatio=8
 * @Author 歐陽思海
 * @Date 2019/12/3 16:00
 * @Version 1.0
 **/
public class Test_01 {

    private static final int M = 1024 * 1024;

    public static void test() {
        byte[] alloc1, alloc2, alloc3, alloc4;
        alloc1 = new byte[5 * M];
        alloc2 = new byte[5 * M];
        alloc3 = new byte[5 * M];
        alloc4 = new byte[10 * M];

    }

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

}

輸入結果:

分析

  • eden:from:to=8:1:1,這個因為前面設定了引數-XX:SurvivorRatio=8
  • 新生代分配了20M的記憶體,所以前面三個byte陣列可以分配,但是,分配第四個的時候,空間不夠,所以,需要進行一次Minor GC,GC之後,新生代從12534K變為598K
  • 前面在新生代分配的記憶體Minor GC之後,進入到了Survivor,但是,Survivor不夠分配,所以進入到了老年代,老年代已用記憶體達到了50%

1.2 回答問題

所以,經過上面的例子我們發現,物件一般優先在新生代分配的,如果新生代記憶體不夠,就進行Minor GC回收記憶體。

點個贊,看一看,好習慣!本文 GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收錄,這是我花了3個月總結的一線大廠Java面試總結,本人已拿騰訊等大廠offer。

2 進入到老年代需要滿足什麼條件

先給出答案,分為幾點。

  • 條件①:大物件直接進入到老年代
  • 條件②:長期存活的物件可以進入到老年代
  • 條件③:如果在Survivor空間中相同年齡所有物件的大小的總和大於Survivor空間的一半,年齡大於等於該年齡的物件直接進入到老年代

2.1 分析條件①

  • 哪些屬於大物件呢?

一般來說大物件指的是很長的字串及陣列,或者靜態物件

  • 那麼需要滿足多大才是大物件呢?

這個虛擬機器提供了一個引數-XX:PretenureSizeThreshold=n,只需要大於這個引數所設定的值,就可以直接進入到老年代。

step1: 解決了這兩個問題,首先,我們不設定上面的引數的例子,將物件的記憶體大於Eden的大小看看情況。

/**
 * @ClassName Test_01
 * @Description 引數:-Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
 * @Author 歐陽思海
 * @Date 2019/12/3 16:00
 * @Version 1.0
 **/
public class Test_01 {

    private static final int M = 1024 * 1024;

    public static void test() {
        byte[] alloc1, alloc2, alloc3, alloc4;
//        alloc1 = new byte[5 * M];
//        alloc2 = new byte[5 * M];
//        alloc3 = new byte[5 * M];
        alloc4 = new byte[22 * M];

    }

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

}

我們發現分配失敗,Java堆溢位,因為超過了最大值。

step2: 下面我們看一個例子:設定-XX:PretenureSizeThreshold=104,857,600,這個單位是B位元組(Byte/bait),所以這裡是100M

/**
 * @ClassName Test_01
 * @Description 引數:-Xms2048M -Xmx2048M -Xmn1024M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:PretenureSizeThreshold=104,857,600
 * @Author 歐陽思海
 * @Date 2019/12/3 16:00
 * @Version 1.0
 **/
public class Test_01 {

    private static final int M = 1024 * 1024;

    public static void test() {
        byte[] alloc1, alloc2, alloc3, alloc4;
//        alloc1 = new byte[5 * M];
//        alloc2 = new byte[5 * M];
//        alloc3 = new byte[5 * M];
        alloc4 = new byte[500 * M];

    }

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

}

發現新生代沒有分配,直接在老年代分配。

注意: 引數PretenureSizeThreshold只對SerialParNew兩款收集器有效。

2.2 分析條件②

進入老年代規則:這裡需要知道虛擬機器對每個物件有個物件年齡計數器,如果物件在Eden出生經過第一次Minor GC後任然存活,並且能夠被Survivor容納,將被移動到Survivor空間中,並且年齡設定為1。接下來,物件在Survivor中每次經過一次Minor GC,年齡就增加1,預設當年齡達到15,就會進入到老年代。

晉升到老年代的年齡閾值,可以通過引數-XX:MaxTenuringThreshold設定。

在下面的例項中,我們設定-XX:MaxTenuringThreshold=1

/**
 * @ClassName Test_01
 * @Description 引數:-Xms2048M -Xmx2048M -Xmn1024M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=1
 * @Author 歐陽思海
 * @Date 2019/12/3 16:00
 * @Version 1.0
 **/
public class Test_01 {

    private static final int M = 1024 * 1024;

    public static void test() {
        byte[] alloc1, alloc2, alloc3, alloc4;
        alloc1 = new byte[300 * M];
        alloc2 = new byte[300 * M];
        alloc3 = new byte[300 * M];
        alloc4 = new byte[500 * M];

    }

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

}

從結果可以看出,from和to都沒有佔用記憶體,而老年代則佔用了很多記憶體。

2.3 分析條件③

條件③是:如果在Survivor空間中相同年齡所有物件的大小的總和大於Survivor空間的一半,年齡大於等於該年齡的物件直接進入到老年代,而不需要等到引數-XX:MaxTenuringThreshold設定的年齡。

例項分析

/**
 * @ClassName Test_01
 * @Description 引數:-Xms2048M -Xmx2048M -Xmn1024M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
 * @Author 歐陽思海
 * @Date 2019/12/3 16:00
 * @Version 1.0
 **/
public class Test_01 {

    private static final int M = 1024 * 1024;

    public static void test() {
        byte[] alloc1, alloc2, alloc3, alloc4;
        alloc1 = new byte[100 * M];
        alloc2 = new byte[100 * M];
        //分配alloc3之前,空間不夠,所以minor GC,接著分配alloc3=900M大於Survivor空間一半,直接到老年代。
        alloc3 = new byte[900 * M];
        
//        alloc4 = new byte[500 * M];

    }

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

}

輸入結果:

分配alloc3之前,空間不夠,所以minor GC,接著分配alloc3=900M大於Survivor空間一半,直接到老年代。從而發現,survivor佔用0,而老年代佔用900M。

3 總結

這篇文章主要講解了JVM記憶體分配與回收策略的原理,回答了下面的這兩個問題。

  • 我們生成的物件最開始在哪分配?Eden?Survivor?還是老年代呢?
  • 進入到老年代需要滿足什麼條件呢?

相關文章