JVM 垃圾收集算法 標記-清楚、標記-復制、標記-整理


摘要

Java程序在運行過程中會產生大量的對象,但是內存大小是有限的,如果光用而不釋放,那內存遲早被耗盡。如C、C++程序,需要程序員手動釋放內存,Java則不需要,是由垃圾回收器去自動回收。

垃圾回收器回收內存至少需要做兩件事情:標記垃圾、回收垃圾。於是誕生了很多算法及垃圾回收器。

垃圾判斷算法

即判斷JVM中的所有對象,哪些對象是存活的,哪些對象可回收的算法。

引用計數算法

在對象中添加一個屬性用於標記對象被引用的次數,每多一個其他對象引用,計數+1,當引用失效時,計數-1,如果計數=0,表示沒有其他對象引用,就可以被回收。

 這個算法無法解決循環依賴的問題。

 

 

可達性分析算法

通過一系列被稱為“GC Roots”的根對象作為起始節點集,從這些節點開始,根據引用關系鏈向下搜索,如果某個對象無法被搜索到,則說明該對象無引用執行,可回收。相反,則對象處於存活狀態,不可回收。

JVM中的實現是找到存活對象,未打標記的就是無用對象,GC時會回收。

 

 

 

 

哪些對象可以作為GC Root呢:

  • 所有Java線程當前活躍的棧幀里指向GC堆里的對象的引用;換句話說,當前所有正在被調用的方法的引用類型的參數/局部變量/臨時值。
  • VM的一些靜態數據結構里指向GC堆里的對象的引用,例如說HotSpot VM里的Universe里有很多這樣的引用。
  • JNI handles,包括global handles和local handles
  • (看情況)所有當前被加載的Java類
  • (看情況)Java類的引用類型靜態變量
  • (看情況)Java類的運行時常量池里的引用類型常量(String或Class類型)
  • (看情況)String常量池(StringTable)里的引用

垃圾回收算法

1、標記-清除算法

概念:

顧名思義,標記-清除算法分為兩個階段,標記(mark)和清除(sweep)。

標記:遍歷所有的GC Roots,然后將所有的GC Roots可達的對象標記為存活的對象。

清除:清除的過程將遍歷所有堆中的對象,將沒有標記的對象全部清除。

圖解:

 對上圖中的黃色部分進行垃圾回收,回收后的截圖如下所示: 

 從圖中可知,進行標記清理后,可用內存增加,但是清除垃圾后的內存地址不連接,出現垃圾碎片。

缺點:

1、執行效率不穩定,如果Java堆中包含大量對象,而且大部分是需要被回收的,這時必須記性大量標記及清除動作,導致標記和清除兩個過程執行效率都隨對象數量增長而降低。

2、內存空間碎片化的問題,標記、清除后會產生大量的不連續內存碎片,空間碎片太可能會導致當以后需要分配大對象時無法找到足夠的連續內存二不得不提前觸發另一次垃圾收集動作。

2、標記-復制算法

概念:

復制算法將內存分為兩個區間,這兩個區間是動態的,在任意一個時間點,所有分配的對象內存只能在其中一個區間(活動區間),另外一個區間就是空閑區間。
當有效內存空間耗盡時,JVM將暫停程序運行,開啟復制算法GC線程。GC線程會將活動區間內的存活對象,全部復制到空閑區間,且嚴格按照內存地址一次排列,與此同時,GC線程將更新存活對象的內存引用地址指向新的內存地址。這個時候空閑內存已經變成了活動區間,垃圾對象全部在原來的活動區間,清理掉垃圾對象,原活動區間就變成了空閑區間。

這種方式內存的代價太高,每次基本上都要浪費一半的內存。於是將該算法進行了改進,內存區域不再是按照1:1去划分,而是將內存划分為8:1:1三部分,較大那份內存是Eden區,其余是兩塊較小的內存區叫Survior區。每次都會優先使用Eden區,若Eden區滿,就將對象復制到第二塊內存區上,然后清除Eden區,如果此時存活的對象太多,以至於Survivor不夠時,會將這些對象通過分配擔保機制復制到老年代中。(java堆又分為新生代和老年代)。

 圖解:

 

 

優點:

1、很好地解決了“標記-清除”算法,內存布局混亂的缺點。

缺點:
1、浪費一半的內存。

2、假設對象存活率為100%,那么“標記-復制”算法的GC過程就是重復的把對象復制一遍,而且將所有的引用地址重置一遍。可以預見的復制所消耗的時間隨着對象存活率達到一定程度將會變成災難。所以“標記-復制”算法使用的場景是可以忍受只是用50%內存,對象存活率非常低

3、標記-整理算法

概念:

標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。

圖解:

 

 

 

 

 

 

優點:

1、彌補了“標記-清除”算法,內存區域分散的缺點
2、彌補了“標記-復制”算法內存減半的代價

缺點:

1、效率不高,對於“標記-清除”而言多了整理工作。

4、分代收集算法

當前商業虛擬機的垃圾收集都采用分代收集。此算法沒啥新鮮的,就是將上述三種算法整合了一下。具體如下:
根據各個年代的特點采取最適當的收集算法

1、在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法。只需要付出少量存活對象的復制成本就可以完成收集。
2、老年代中因為對象存活率高、沒有額外空間對他進行分配擔保,就必須用標記-清除或者標記-整理。

測試案例

以下測試采用的是Serial加Serial Old收集器組合。

查看當前jdk默認額收集器使用以下語句。

java -XX:+PrintCommandLineFlags -version

執行結果:

1、對象優先在Eden分配

測試代碼:

/**
 * @Description 對象優先在Eden分配
 * VM參數:-verbose: gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
 */
public class AllocationMemory {
    private  static final int _1MB=1024 * 1024;

    public static void main(String[] args) {
        byte[] allocation1,allocation2,allocation3;
        allocation1=new byte[3 * _1MB];
        allocation2=new byte[2 * _1MB];
        allocation3=new byte[5 * _1MB];
    }
}

測試結果:

 原因分析:

* eden 8M
* from 1M
* to 1M
* 老年代 10M

allocation1=new byte[3 * _1MB];

執行此行代碼時,新生代Eden為空,滿足分配3M的需求
執行完成后,allocation1指向的對象在新生代Eden區;

allocation2=new byte[2 * _1MB];

執行此行代碼時,新生代Eden包含3M的對象,滿足分配2M的需求
執行完成后,allocation2指向的對象在新生代Eden區;

allocation3=new byte[5 * _1MB];

對allocation3分配內存時,發現Eden已經占用5MB,剩余的空間不滿足分配allocation3需要的5MB內存,因此發生了Minor GC。
從測試結果的圖中Minor GC發生時內存變化可以看出,新生代內存從7679K變化成608K,新生代的內存使用量基本清空了。
而整個堆的大小從7679K變化成5728K,堆區的使用量仍然包含5MB的數據,可以判斷出之前的5MB數據從新生代復制到了老年代了。
又因為新生代的servivor區的from區和to區分別各站1MB的內存,根本不足以放下2MB和3MB的對象。所以對象優先在Eden區分配。
執行垃圾回收后,新生代Eden區清空,滿足分配5M的需求,allocation3指向的對象在新生代Eden區。
因此程序執行完成后,堆區的情況是:
eden space 8192K,  63%  --包含allocation3 指向的5MB對象
the space 10240K,  50%  --包含 allocation1、allocation2指向的對象。           

2、大對象直接進入老年代

測試代碼:

/**
 * @Description 大對象直接進入老年代
 * VM參數:-verbose: gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
 * -XX:PretenureSizeThreshold=3145728
 */
public class PretenureSizeThreshold {
    private  static final int _1MB=1024 * 1024;

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

測試結果:

原因分析:

虛擬機提供了-XX:PretenureSizeThreshold參數,制定大於該設置值的對象直接進入老年代。

從測試結果中可以看出老年代占用了4MB空間,新生代Eden區占用不足4MB,因此可以判斷生成的4MB對象是在老年代。這是因為-XX:PretenureSizeThreshold被設置為3MB,因此超過3MB的對象都會直接在老年代進行分配。

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

測試代碼

public class TenuringThreshold {
    public static final int _1Mb =1024*1024;

    public static void main(String[] args) {     
        byte[] alloctation1=new byte[_1Mb/4];
        byte[] alloctation2=new byte[8 * _1Mb];
        byte[] alloctation3=new byte[8 * _1Mb];
        alloctation3=null;
        alloctation3=new byte[8 * _1Mb];

    }
}

測試結果:

1、VM參數:-verbose: gc -Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution

 

 2、VM參數:-verbose: gc -Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution

原因分析:

1、VM參數:-verbose: gc -Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution

eden 16M

from 2M

to 2M

老年代 20M

 程序執行完后, allocation1 指向的對象 在 老年代 , allocation2 在老年代, allocation3 先指向的對象被垃圾回收了, allocation3后指向的對象在eden區。

byte[] alloctation1=new byte[_1Mb/4];
* 執行完后,allocation1 指向的對象在新生代的Eden區;
byte[] alloctation2=new byte[8 * _1Mb];
* 執行完后,allocation2 指向的對象在新生代的Eden區;
byte[] alloctation3=new byte[8 * _1Mb];
* 因為eden區不足需要分配的8M空間,所以觸發了young GC,
* GC發生后,allocation1 指向的對象從eden區復制到servivor區,allocation2所指向的對象, 因為servivor區空間不夠分配,觸發空間擔保,進入老年代。
* GC完成后 alloctation3 指向的對象在新生代的Eden區;
alloctation3=null;
alloctation3=new byte[8 * _1Mb];
* 因為Eden區不足需要分配的8M空間,所有觸發了young GC
* GC發生后,eden區中的8M對象因為GC Root未可達,所有判斷為死對象,被垃圾回收器回收, allocation1 指向的對象因未達到1次的年齡限制,所以復制到了老年代。
* allocation2所指向的對象仍然指向老年代。
* GC完成后,eden區滿足8M對象分配的空間要求,alloctation3 指向的對象在新生代的Eden區;

 2、VM參數:-verbose: gc -Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution

*程序執行完后, allocation1 指向的對象 在 servivor區 , allocation2 在老年代, allocation3 先指向的對象被垃圾回收了, allocation3后指向的對象在eden區。

byte[] alloctation1=new byte[_1Mb/4];
* 執行完后,allocation1 指向的對象在新生代的Eden區;
byte[] alloctation2=new byte[8 * _1Mb];
* 執行完后,allocation2 指向的對象在新生代的Eden區;
byte[] alloctation3=new byte[8 * _1Mb];
* 因為eden區不足需要分配的8M空間,所以觸發了young GC,
* GC發生后,allocation1 指向的對象從eden區復制到servivor區,allocation2所指向的對象, 因為servivor區空間不夠分配,觸發空間擔保,進入老年代。
* GC完成后 alloctation3 指向的對象在新生代的Eden區;
alloctation3=null;
alloctation3=new byte[8 * _1Mb];
* 因為Eden區不足需要分配的8M空間,所有觸發了young GC
* GC發生后,eden區中的8M對象因為GC Root未可達,所有判斷為死對象,被垃圾回收器回收, allocation1 指向的對象因未達到15次的年齡限制,仍然在servivor區。
* allocation2所指向的對象仍然指向老年代。
* GC完成時,eden區滿足8M對象分配的空間要求,alloctation3 指向的對象在新生代的Eden區;

4、動態年齡判斷

測試代碼:

/**

 * VM參數:-verbose: gc -Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
 *  *  *  -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution

 * @Version 1.0
 */
public class SpaceGuarantee {
    public static final int _1MB=1024 * 1024;

    public static void main(String[] args) {

        byte[] alloctation1=new byte[_1MB/4];
        //屏蔽#1此行代碼后,就不會觸發動態年齡判斷了。
        //byte[] alloctation2=new byte[_1MB/4]; //#1
        byte[] alloctation3=new byte[8 * _1MB];
        byte[] alloctation4=new byte[8 * _1MB];
        alloctation4=null;
        alloctation4=new byte[8 * _1MB];
    }
}

測試結果

1、未屏蔽 #1 處代碼

 

 2、屏蔽 #1 處代碼

原因分析:

通過兩個結果的對比,發下在沒有屏蔽#1處代碼的情況下,Servivor區占用為0%,而老年代比預期占用的高,也就說明了survivor區的數據在經歷第二次GC的年齡計算就全部轉入到老年代了,並沒有等到15歲的臨界年齡。

而屏蔽了#1處代碼的情況下,Servivor區占用為38%,而老年代與預期值相似。說明servivor區的數據經歷第二次GC的年齡計算沒有復制到老年代。

這是因為沒有屏蔽#1處代碼的情況下,allocation1、allocation2兩個對象加起來達到了512K,並且他們是同年齡的,滿足同年對象達到Servivor區空間一半的規則。

 

 byte[] alloctation1=new byte[_1MB/4];
* 新生代Eden區滿足256K對象的空間需求, allocation1 指向的對象被分配到新生代的eden區
byte[] alloctation2=new byte[_1MB/4];
* 新生代Eden區滿足256K對象的空間需求, allocation2 指向的對象被分配到新生代的eden區
byte[] alloctation3=new byte[8 * _1MB];
* 新生代Eden區滿足8MB對象的空間需求, allocation3 指向的對象被分配到新生代的eden區
byte[] alloctation4=new byte[8 * _1MB];
* 需要分配8MB的內存到新生代的eden區,Eden區空間不足,觸發young GC,
* GC發生后,allocation1 指向的對象被復制到新生代的servivor區,allocation2指向的對象被復制到新生代的servivor區,
* allocation3指向的對象因為servivor區不足以放下8MB的對象,所以觸發空間擔保,被復制到老年代中。
* GC完成后,allocation4指向的對象被分配到新生代的Eden區。
alloctation4=null;
alloctation4=new byte[8 * _1MB];
* 需要分配8MB的內存到新生代的eden區,Eden區空間不足,發生young GC,
* GC發生后,allocation1 指向的對象本應該繼續保留在servivor區中,但是因為servivor區中相同年齡所有對象的大小的總和大於servivor空間的一半,因此觸發了動態年齡判斷,所以被復制到老年代。
* allocation2 指向的對象本應該繼續保留在servivor區中,但是因為servivor區中相同年齡所有對象的大小的總和大於servivor空間的一半,因此觸發了動態年齡判斷,所以被復制到老年代。
* eden區中的8M對象因為GC Root未可達,所有判斷為死對象,被垃圾回收器回收。
* GC完成后,eden區滿足8M對象分配的空間要求,alloctation4 指向的對象在新生代的Eden區;

5、空間分配擔保

測試代碼:

/**
 *  VM參數:-verbose: gc -Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
 *  eden 16M
 *  from  2M
 *  to  2M
 *  老年代   20M
 *
 * @Version 1.0
 */
public class HandlePromotion {
    public static final int _1MB = 1024*1024;

    public static void main(String[] args) {
        byte[] allocation1=new byte[4*_1MB];
        byte[] allocation2=new byte[4*_1MB];
        byte[] allocation3=new byte[4*_1MB];
        allocation1=null;

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

        allocation4=null;//#1
        allocation5=null;//#2
        allocation6=null;//#3

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

}

測試結果

1、未注釋#1,#2,#3 三行代碼的情況

2、注釋#1,#2,#3 三行代碼的情況

原因分析:

對比 未注釋#1,#2,#3 三行代碼的情況 與注釋#1,#2,#3 三行代碼的情況 發現
未注釋#1,#2,#3 三行代碼的情況下 發生了兩次Minor GC。
而注釋#1,#2,#3 三行代碼的情況下 發生了兩次Minor GC,並且第二Minor GC執行時因空間擔保失敗而引發了一次Full GC。

 

 

執行byte[] allocation4=new byte[4*_1MB]代碼,需要向Eden區申請4MB內存空間,因為Eden空間不足分配,觸發第一次GC,
在發生minor GC之前,虛擬機檢查老年代20M內存,之前沒有晉升到過老年代的對象,因此滿足需求,所有發生Minor GC。
在此次GC中,Eden區會清除掉原來allocation1指向的4MB內存區,allocation2、allocation3指向的內存因Servivor無法滿足分配
因此被復制到新生代。此次從新生代復制到老年代的內存為8MB。


未注釋#1,#2,#3 三行代碼的情況下,執行byte[] allocation7=new byte[4*_1MB]代碼,需要向Eden區申請4MB內存,但Eden區內存無法滿足,因此觸發了第二次GC,
在發生minor GC之前,虛擬機先檢查老年代最大可用空間為12M,大於晉升的平均值8M,因此滿足空間擔保需求,所有發生Minor GC。
在此次GC中,Eden區中 allocation4 、allocation5、allocation6所指向的對象,因為成為了非存活對象,所有被清楚了。
執行完GC后,allocation7 指向的對象被分配到Eden區。

 

 

注釋#1,#2,#3 三行代碼的情況下,執行byte[] allocation7=new byte[4*_1MB]代碼,需要向Eden區申請4MB內存,但Eden區內存無法滿足,因此觸發了第二次GC,
在發生minor GC之前,虛擬機先檢查老年代剩余最大可用空間大於晉升的平均值8M,因此滿足空間擔保需求,所有發生Minor GC。
在此次GC中,Eden區中 allocation4 、allocation5、allocation6所指向的對象都存活對象,所以需要復制到Servivor區,但Servivor區不足以放下4MB大小的對象,需要晉升到老年代。

但老年代剩余可用空間不夠放下此次晉升的對象。因此引發了Full GC。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM