第三章 垃圾回收器與內(nèi)存分配策略

1.Copying算法補充

目前主流虛擬機對新生代的對象回收都是用的這種算法。IBM研究表明,新生代中的對象98%都是“朝生夕死”的,所以并不需要按照1: 1來劃分內(nèi)存,而是將內(nèi)存分為一塊較大的Eden和兩塊較小的Survivor,每次使用使用Eden和一塊Survivor。當回收時,將Eden和Survivor中活著的對象全部復制到另一塊Survivor中,然后清理掉Eden和原先的那一塊Survivor中全部的對象。HotSpot虛擬機默認的分配空間是8: 1,當然我們沒有辦法保證每次回收都只有不多于10%的對象存活,當Survivor空間不夠用時,需要依賴老年代進行分配擔保。

2. 內(nèi)存分配與回收策略

2.1 對象優(yōu)先在Eden分配

大多數(shù)情況下,對象在新生代Eden中分配,當Eden區(qū)沒有空間時,虛擬機會發(fā)起一次MinorGC(在新生代中的垃圾回收動作)。來看書中提供的這個例子

/**
 * Created by cwj on 17-6-3.
 * VM:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
 */
public class TestMinorGC {

    public static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] allocatioon1,allocatioon2,allocatioon3,allocatioon4;

        allocatioon1 = new byte[2 * _1MB];
        allocatioon2 = new byte[2 * _1MB];
        allocatioon3 = new byte[2 * _1MB];

        allocatioon4 = new byte[4 * _1MB];
    }
}

我使用的是idea+jdk1.8,所以與書中不同的是,要設(shè)置虛擬機使用Serial/Serial Old收集器,-XX:+UseSerialGC,如果不設(shè)置的話,idea會使用PS回收器,這樣效果就不一樣了??纯磮?zhí)行效果

執(zhí)行效果

代碼中,嘗試分配3個2M大小和1個4M大小的對象,運行時通過設(shè)置虛擬機參數(shù)將堆大小定位20M且不可擴展,其中新/老生代各占10M。并且定義了Eden與Survivor比例是8:1,從輸出結(jié)果也能看到:
eden space 8192K, from space 1024K, to space 1024K,其中新生代總共可用空間是9216KB(Eden+一個Survivor)。
  執(zhí)行到分配allocatioon4的空間是發(fā)生了一次MinorGC,結(jié)果是新生代從7581kb變到336kb,而總內(nèi)存是7518kb變到6480kb,幾乎沒怎么變,因為1,2,3這三個對象都是存活的,并沒有被回收。這次GC發(fā)生的原因是給4分配內(nèi)存的時候,發(fā)現(xiàn)Eden已經(jīng)被占用了6M,剩余的空間不足以分配4所需要的4M空間,所以發(fā)生了MinorGC。期間虛擬機又發(fā)現(xiàn)無法將3個2M大小的對象全部放入Survivor空間(因為Survivor只有1M),所以只能通過擔保機制,將3個對象轉(zhuǎn)移到老年代中。
  這次GC結(jié)束后,4M的allocation4對象被順利的分配到了Eden中,因此程序執(zhí)行結(jié)果是Eden占用了4M,Survivor空閑,老年代被占用了6M。

2.2 大對象直接進入老年代

所謂的大對象是指需要連續(xù)大量內(nèi)存空間的Java對象,最典型的大對象就是那種很長的字符串和數(shù)組。經(jīng)常出現(xiàn)大對象容易導致內(nèi)存還有不少空間時就提前出發(fā)垃圾回收器以獲得足夠多的連續(xù)空間來安置他們
  虛擬機提供了一個 -XX:PretenureSizeThreshold 參數(shù),大于這個參數(shù)的對象直接進入老年代。這樣做的目的是為了避免Eden和兩個Survivor之間發(fā)生大量的內(nèi)存復制。

/**
 * Created by cwj on 17-6-3.
 * VM:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
 *    -XX:PretenureSizeThreshold=3145728
 *
 */
public class TestMinorGC {

    public static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] allocatioon;
        allocatioon = new byte[4 * _1MB];
    }

}
執(zhí)行結(jié)果.png

結(jié)果很清楚,因為我們預先設(shè)置了參數(shù)-XX:PretenureSizeThreshold=3145728,所以大于3M的對象直接就被分到了老年代中,所以老年代占了4M,新生代空閑。

2.3 長期存活的對象將進入老年代

虛擬機采用了分代收集的思想來管理內(nèi)存,那么內(nèi)存回+收時就必須知道哪些對象應(yīng)該放在新生代,哪些放在老年代。為了做到這點,虛擬機給每個對象定義了一個年齡計數(shù)器。如果對象在Eden出生并經(jīng)過一次GC后仍然存活,并且能被Survivor容納的話,將被移動到Survivor空間中,并且對象年齡設(shè)置為1。對象在Survivor中每熬過一次GC,年齡就增大一歲,當它的年齡增大到一定歲數(shù)時(默認15歲),就會被移到老年代。當然這個默認值可以通過修改參數(shù) -XX:MaxTenuringThreshold的值來改變。

/**
 * Created by cwj on 17-6-3.
 *
 * VM:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
 *    -XX:MaxTenuringThreshold=1
 *    -XX:+PrintTenuringDistribution
 */
public class TestTenuringThreshold {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {

        byte[] allocation1,allocation2,allocation3;

        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[4 * _1MB];
        allocation3 = new byte[4 * _1MB];
        //第一次GC

        allocation3 = null;
        allocation3 = new byte[4 * _1MB];
        //第二次GC
    }
}

執(zhí)行結(jié)果

我們分析一下:首先1,2都被分到Eden中,當3第一次來時,Eden不夠4M了,所以進行一次GC,將1放入Survivor,Age+1。又因為Survivor只有1M,不夠4M,所以直接擔保機制,2放入老年代。所以Eden從5726k->616k,總內(nèi)存基本沒變。
  當?shù)诙?進來時,發(fā)生第二次GC,將原來存在Eden中的3釋放(賦值為null了),Survivor中的1已經(jīng)1歲了,所以存入老年代,這時Eden剛釋放完3,下一個3還沒有進來,所以Eden從4712k->0,總內(nèi)存由8808k->4706k,GC過后將新的3存入Eden。兩次GC過后。Eden中存著的是3,老年代中存著的是2和1。
  如果將參數(shù) -XX:MaxTenuringThreshold值改為15,兩次GC過后應(yīng)該是Eden中存著3,Survivor中存著1,老年代中存著2.(理論是這樣,然而我的idea中跑的結(jié)果還是跟參數(shù)為1的情況一樣,很納悶。。。)

2.4 動態(tài)對象年齡判定

為了能更好的適應(yīng)不同程序的內(nèi)存情況,虛擬機并不是永遠要求對象的年齡必須達到MaxTenuringThreshold參數(shù)的值時才能晉升到老年代。如果在Survivor中所有相同年齡的對象大小的總和超過Survivor空間的一半,則年齡大于或等于該年齡對象就可以直接進入老年代,無需等到參數(shù)設(shè)定的年齡

/**
 * Created by cwj on 17-6-3.
 *
 * VM:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
 *    -XX:MaxTenuringThreshold=15
 *    -XX:+PrintTenuringDistribution
 */
public class TestTenuringThreshold2 {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {

        byte[] allocation1,allocation2,allocation3,allocation4;

        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[_1MB / 4];

        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];
        //第一次GC

        allocation4 = null;
        allocation4 = new byte[4 * _1MB];
        //第二次GC
    }
}

執(zhí)行結(jié)果

可以看到結(jié)果中Survivor仍然是0,而老年代比預計的增加了6%,說明1,2并沒有等到15歲就直接進入了老年代,因為這兩個對象加起來的和已經(jīng)達到了Survivor空間的一半,并且他們是同年的。我們只要注釋掉一個,另外一個就不會進入老年代了。

2.5 空間分配擔保

新生代不夠放了就往老年代里面挪,那老年代也有不夠的時候啊,這時候怎么辦呢?如果老年代的連續(xù)空間大于新生代對象的總大小,或者歷次晉升的平均大小,就可以進行安全的MinorGC,如果不大于那么就會執(zhí)行一次FullGC來釋放老年代的空間。

/**
 * Created by cwj on 17-6-3.
 *
 * VM:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
 *    -XX:+PrintTenuringDistribution
 */
public class TestTenuringThreshold3 {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {

        byte[] allocation1,allocation2,allocation3,allocation4,allocation5,allocation6,allocation7;

        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation1 = null;

        allocation4 = new byte[2 * _1MB];
        allocation5 = new byte[2 * _1MB];
        allocation6 = new byte[2 * _1MB];

        allocation4 = null;
        allocation5 = null;
        allocation6 = null;
        allocation7 = new byte[2 * _1MB];
    }
}

執(zhí)行結(jié)果
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容