1. 程式人生 > >java記憶體分配

java記憶體分配

java對記憶體的回收和收集器的主要思想我在前一篇部落格進行了詳細的描述,這裡主要講的是java如何實現對記憶體的管理的,在講解之前我們需要做的是理解如何配置jvm引數和引數的意義,下面我也會提到一些引數的作用和使用的場合,並會達到什麼效果,但前提是必須瞭解jvm中堆記憶體是如何分配的,也可以看我之前的一篇文章:

以下的部分資料也可以看看周志明老師寫的《深入理解java虛擬機器》這本書,對java虛擬機器進行了詳細的介紹

1、物件優先在Eden區進行分配

大多數情況下,物件都是在新生代Eden區分配,當Eden區沒有足夠的空間進行分配時,虛擬機器就會發生一次MinorGC(新生代垃圾回收),所謂MinorGC就是在給物件分配記憶體時,發現此時Eden區已經沒有任何空間能夠容納這個物件,就會把Eden區中存活的物件放入Survivor區,但是如果Survivor區不能容納這些存活的物件,就只能通過分配擔保機制提前轉移到老年代去,

舊生代用於存放新生代中經過多次垃圾回收 (也即Minor GC) 仍然存活的物件,下面是一個圖示可以非常完美的說明,Form Space和To Space分別就是兩個Survivor區:


可以使用一段程式碼來驗證當新生代的記憶體不足以容納一個物件時,就會發生一次Minor GC,也就是將新生代中存活的物件放入到老年代中,寫程式碼之前我們需要配置一下jvm引數,方法網上有很多,比較流行的方法是右鍵類選Run->Run as Configurations,選擇Arguments,在vm arguments中寫入: -Xloggc:E:\\gc.log -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseParNewGC
解釋一下每句的含義: -Xloggc:E:\\gc.log:生成日誌檔案寫入E盤的gc.log檔案裡 -Xms20M -Xmx20M -Xmn10M:設定初始堆大小為20M,不擴充套件,新生代堆為10M
-XX:+PrintGCDetails:列印一份詳細日誌
-XX:SurvivorRatio=8:設定Eden區域可Survivor區域的比例為8:1
-XX:+UseParNewGC:這裡使用ParNew和Serial Old的組合 然後設計程式碼如下:
public class Test {
    //設計一個byte陣列長度,大小為1024*1024,其實就是1MB
    private static final int _1MB=1024*1024;
    public static void testAllocation(){
         
        byte[] allocation1,allocation2,allocation3,allocation4;
        //為四個byte陣列分別分配2M,2M,2M,4M的空間
        allocation1=new byte[2*_1MB];
        allocation2=new byte[2*_1MB];
        allocation3=new byte[2*_1MB];
        /*
        當為第四個陣列分配記憶體時已經沒有多餘的記憶體可以容納下這個陣列了
         * 所以就會在新生代Eden區域進行一次垃圾回收,但此時Survivor區域也不能容納
         * 就會將上面的三個陣列全部放入到老年代中進行擔保
         * 這就是所謂的進行一次MinorGC(新生代垃圾回收)
        */ 
        allocation4=new byte[4*_1MB];
    }
     public static void main(String[] args) {
         Test.testAllocation();
     }
}

看一下日誌的結果:


從上面的日誌中可以清楚的看到總的PSYoungGen(新生代)的空間大小為一個Eden區域加上一個Survivor區域的和,新生代eden區佔用了4M多的記憶體,因為eden區可能本身就被佔用了部分記憶體,但是老年代中有60%的記憶體,也就是6M,說明剛開始的三個變數共6M的記憶體都被放入到了老年代中,而新生代只容納了最後一個變數4M的記憶體,說明發生了一次MinorGC將新生代中存活的物件都被放入到了老年代中。

2、大物件直接進入老年代:

所謂的大物件就是那種佔據大量連續空間的物件,例如長字串或者大陣列,這種物件會佔據極大的記憶體空間,經常出現大物件可能會導致記憶體還有不少空間是就提前出發垃圾收集以獲取足夠的連續空間來安置他們,這對於我們的程式肯定是極為不利的,如何避免大物件,jvm的一個做法就是通過設定-XX:PretenureSizeThreshold引數來實現對大物件的處理,當我們的物件超過這個值時,jvm會直接將這個物件放入到老年代中,這可以避免在新生代中利用複製演算法進行大量的複製,因為新生代區是用複製演算法實現的,下面的例子可以說明物件過大是將物件直接放入到了老年代: 首先jvm引數中加上-XXPretenureSizeThreshold=3145728,設定最大允許的物件大小為3M,這裡不能直接寫3M 程式碼實現:
public class Test {
    //設計一個byte陣列長度,大小為1024*1024,其實就是1MB
    private static final int _1MB=1024*1024;
    public static void testAllocation(){
         
        byte[] allocation1;
        /*
         * 直接放入一個4M位元組的陣列,這個時候由於超過了-XX:PretenureSizeThreshold指定的
         * 範圍3M,這個引數只能使用位元組的形式表示,這個時候會直接將這個4M陣列放入到老年代中
         */
        allocation1=new byte[4*_1MB];
    }
     public static void main(String[] args) {
         Test.testAllocation();
     }
}
列印日誌內容:


從上面的日誌中可以看到當直接放入一個4M的資料時,由於超過了PretenureSizeThreshold引數3M的限制,而被直接放到老年代中去了,因為老年代中剛好也增加了4M的記憶體

3、長期存活的物件將直接被放入到老年代:

我們知道垃圾收集器回收物件是通過分代收集的思想實現的,那麼記憶體必須知道將哪些物件應該放入老年代,哪些物件放入新生代,jvm給出了一中物件年齡的計數器,上面提到到eden區域沒有足夠的記憶體空間存放我們定義的物件時,就會進行一次MinorGC,但是如果Survivor區域可以容納的話,就會被移動到Survivor區域,並將物件的年齡設定成1,物件沒熬過一次MinorGC,年齡就會加1,當年齡超過一個閾值就會被放入老年代中,我們可以通過-XX:MaxTenuringThreshold=10來設定閾值,通過-XX:+PrintTenuringDistribution可以打印出比較清楚的日誌,可以看出閾值增加時的記憶體變化,下面是一個例項,分別是閾值為1和閾值為10的情況:

public class Test {
    //設計一個byte陣列長度,大小為1024*1024,其實就是1MB
    private static final int _1MB=1024*1024;
    public static void testAllocation() throws InterruptedException{
        System.gc();//做一次垃圾清理,保證Survivor區域空間不會超過1/2
        Thread.sleep(500);//休眠500ms,保證清理已經結束了
        @SuppressWarnings("unused")
        byte[] allocation1,allocation2,allocation3,allocation5;
        allocation1=new byte[_1MB/4];
        allocation2=new byte[4*_1MB];
        allocation3=new byte[4*_1MB];
        allocation3=null;
        allocation3=new byte[4*_1MB];
    }
     public static void main(String[] args) throws InterruptedException {
         Test.testAllocation();
     }
}
閾值為1時的情況,可以看到當進行第一次垃圾回收時survivor區域被放入了256K的記憶體,但是第二次回收時survivor空間變為了0K,並與閾值為10的情況相比,老年代還多了3%的記憶體,初略計算剛好是256KB左右,說明新生代survivor區域的內容被放入到了老年代中:


閾值為10時的情況,可以看到在發生兩次收集時survivor區域的記憶體始終是256KB:



4、動態年齡物件判斷:

jvm其實不是一定要求物件的年齡必須達到MaxTenuringThreshold才能晉升到老年代,如果Survivor空間中相同年齡所有物件的大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就會直接進入到老年代,而不需要等到MaxTenuringThreshold中要求的年齡,比如上方的例項中可以看到,我們在做操作時進行了一次垃圾清理,可以儘量保證survivor中的記憶體被清理,否則可能會出現survivor區域的記憶體大於1/2而導致直接將survivor中相同年齡的物件放入老年代,也就不會出現想要的結果,例如在上方的程式碼allocation1後面allocation5分配一個256kb的空間,最後得到結果如下所示:


結果顯示當插入512KB的記憶體時,此時survivor中年齡相等的有allocation1和allocation5,並且他們之和等於survivor區域1/2的大小,所以即使沒有等到MaxTenuringThreshold中要求的年齡,這兩個物件仍然被放置到了老年代中進行擔保,因為老年代中的記憶體剛好比上面一個閾值為10的例項增加了5%左右的記憶體,大約就是512KB左右的記憶體空間。

5、空間分配擔保:

在發生Minor GC時,虛擬機器會檢測之前每次晉升到老年代的平均大小是否大於老年代剩餘空間的大小,如果大於,就改為直接進行一次FullGC(或者叫Major GC ,也就是老年代垃圾收集),如果小於就會檢視HandlePeomotionFailure設定是否允許擔保失敗,如果允許,那隻會進行Minor GC;如果不允許,則也要改為進行一次FullGC。當出現大量Minor GC物件仍然存活的情況下,並且Survivor無法容納這些物件的時候,就需要讓老年代進行擔保,但前提是老年代有足夠的空間來容納,但是一共有多少物件存活下來是不能明確知道的,所以只好取之前每一次回收晉升到老年代的物件容量的平均值,與老年代的剩餘空間進行比較,決定是否進行FullGC來讓老年代騰出更多空間,當我們打HandlePeomotionFailure開關開啟時,可以避免進行過多的FullGC,畢竟FullGC消耗的時間幾乎是MinorGC的十倍左右。