Java堆空間詳解之新生代和老年代

Java運行時內存區主要分爲 運行時棧(虛擬機棧)、本地方法棧、程序計數器、堆空間、方法區(JDK1.8之後是元空間),今天來聊一聊我們的堆空間.

一個對象或者數組的創建是在堆空間中完成的,堆的大小是有限的(固定的),所以,必不可少的我們要考慮一下堆的空間分配問題和對象的分配問題.

空間分配問題:

堆空間默認的初始化內存最小值爲 系統內存/64,最大值爲系統內存/4;我們可以通過命令 -Xms666m -Xmx666m,來將堆空間的初始化大小最小值和最大值都設置爲666m;

public class HeapSpaceInitial {
    public static void main(String[] args) {

        //返回Java虛擬機中的堆內存總量
        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        //返回Java虛擬機試圖使用的最大堆內存量
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

        System.out.println("-Xms : " + initialMemory + "M");
        System.out.println("-Xmx : " + maxMemory + "M");
    }
}

輸出結果如下:

因爲我的系統內存是16G,所以對應的 243M 和3602M.

我們通過

設置堆空間的最小和最大內存之後,輸出結果如下:

看到這裏大家也許會有疑問,爲什麼這裏輸出的數據會跟我們的實際分配的數據有一定的差別呢? 來,我們來詳細探究一下.運行起來剛纔的程序,然後我們通過命令行來進行查看

這裏 可以看到具體的堆空間的每個空間分配的大小情況.大家用電腦中的計算機進行加一下,可以發現.

S0C(S0區)+S1C(S1區)+EC(Eden區)+OC(老年代區) = 681984/1024 = 666M~ 

咦,這裏怎麼會又一樣了呢, 那我們再做一下試驗,S0C(S0區)+EC(Eden區)+OC(老年代區) = 653824/1024=638M,這個結果跟我們輸出的結果是一樣的.

這是因爲在JVM規定當中,新生代區分爲Eden區(伊甸園區)、S0區(Survivor0區,倖存者0區)S1區(Survivor1區,倖存者1區) ,而在兩個倖存者區中只有一個空間會存放數據,另外一個空間是空的,所以在計算堆空間大小的時候,我們只計算一個空間大小.這也就是爲什麼會有一定偏差的原因

 通過上面的實驗結果,我們也可以發現新生代區和老年代區的默認分配大小比例爲1:2(222M:444M).我們可以通過參數-XX:NewRatio=a,來設置新生代和老年代的大小比例爲1:a,

 

我們在通過隨便跑一個沒有設置過的分配比例的代碼(如圖,我隨便用了一個EdenSurvivorTest方法),

代碼如下:

public class EdenSurvivorTest {
    public static void main(String[] args) {
        System.out.println("進行測試,進行測試");
        try {
            //睡一會,方便我們查看命令
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

然後再cmd中打入命令jsp,查看當前運行的方法對應的pid,然後用命令 jinfo -flag NewRatio pid 來查看,得出結論,默認的新生區和老年區的大小比例爲1:2.

對象分配過程:

首先,我們說一下,一個對象的分配一般過程,如圖:

對象最一開始創建的時候是分配在在Eden區中的,當我們不斷的創建對象,Eden區不可避免的會存放滿,這個時候就會觸發YGC,而此時根據算法得出那些對象是還有引用的,這個時候就會將這些對象放入到Survivor區中的S0區(隨機放入S0和S1,此處我們用S0舉例子),此時S0區就是FROM區,S1區就是TO區. 這些放入到Survivor區中的對象會有一個age(年齡計數器),記錄這個對象在Survivor區中存放的迭代次數.每次迭代之後,age+1.

隨後在不斷迭代這個過程中,如果Survivor區中有兌現給的age到達了 我們設置的-XX:MaxTenuringThreshold 次數(默認是15,但是不同的JVM和不同的GC ,此數值都不相同),此時我們就會將Survivor區中的對象放到到Old(老年代區).

通過代碼來驗證一下:

public class HeapInstanceTest {
    //創建一個隨機大小的字節數組
    byte[] buffer = new byte[new Random().nextInt(1024 * 200)];

    public static void main(String[] args) {
        ArrayList<HeapInstanceTest> list = new ArrayList<HeapInstanceTest>();
        while (true) {
            list.add(new HeapInstanceTest());
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

程序最終會以OOM異常結束.在結束前,我們可以使用JDK自帶的Java VisualVM工具來查看此時內存的情況:

由圖中可以看出.Eden區的大小是不斷增多到達滿值之後觸發GC一下降低到0,然後繼續增多....而對應的,Eden區GC之後,有引用的對象放入到了S0區,然後迭代次數多了之後Old區的對象開始多了起來.

我們再說一下對象分配的特殊情況,如圖:

當我們創建的新對象太大,超出了Eden區大小或者是本來Eden區就已經存放了一些數據的時候,此時新對象創建,判斷Eden區放不下,這時候就會觸發YGC,如果此時Eden區放得下,那麼我們就會將之放入Eden區,這也是我們上面說的一般情況中的.

而如果是因爲創建的新對象太大,YGC之後,Eden區依然放不下的話,我們就判斷Old區是否能放下,如果能放下的話,直接分配到Old區,而如果放不下的話,我們就會觸發FGC,FGC之後如果能放下,我們就會放入到Old區;FGC之後Old區依然放不下的話,JVM就會直接報出OOM異常,退出程序.

當我們對象放入Eden區之後,Eden區滿,觸發YGC,此時會將還有引用的對象放入到Survivor區,如果此時Survivor區放不下的話,就會直接晉升到老年代.

還有一種情況是,如果在Survivor區中相同年齡的對象的所有大小之和超過Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無需等到-XX:MaxTenuringThreshold中要求的年齡。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章