JVM 垃圾回收機制(GC)總結

一、概述

    說起垃圾收集(Garbage Collection),大多數人都會想起Java,這項技術從始至終伴隨着Java的成長,但事實上GC的出現要早於Java,它誕生於1960年MIT的使用動態分配和垃圾回收技術的語言Lisp。經過近60年的發展,目前內存的動態分配和內存回收技術已經非常成熟了,所有的垃圾回收已經自動化,經過迭代更新,自動回收也經過反覆優化,效率和性能都非常可觀。

爲什麼要了解GC?

    在你排查內存溢出、內存泄漏等問題時,以及程序性能調優、解決併發場景下垃圾回收造成的性能瓶頸時,就需要對GC機制進行必要的監控和調節。

二、怎樣標識哪些對象“已死”?

    既然名叫垃圾回收,那麼哪些對象成爲“垃圾”呢?已經不再被使用的對象便視爲“已死”,就應該被回收。在Java中,GC只針對於堆內存,Java語言中不存在指針說法,而是叫引用,在堆內存中沒有被任何棧內存引用的對象應該被回收

1.引用計數算法

    引用計數算法是判斷對象是否存活的算法之一:它給每一個對象加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器爲0的對象就是不可能被使用的,即將被垃圾回收器回收。

缺點:無法解決對象減互相循環引用的問題。即當兩個對象循環引用時,引用計數器都爲1,當對象週期結束後應該被回收卻無法回收,造成內存泄漏

 

public class GcTest {
    public static void main(String[] args) {        MyObject myObject_1 = new MyObject();        MyObject myObject_2 = new MyObject();                myObject_1.instance = myObject_2;        myObject_2.instance = myObject_1;
        myObject_1 = null;        myObject_2 = null;  
        //  對象循環引用,當時用引用計數算法時,無法回收這兩個對象        System.gc();    }        static class MyObject{        Object instance;    }}

2.可達性分析算法

    目前主流使用的都是可達性分析算法來判斷對象是否存活。算法基本思路:以“GC Roots”作爲對象的起點,從此節點開始向下搜索,搜索所走過的路徑成爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。

哪些對象可作爲GC Roots?

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象;

  • 方法區中類靜態屬性引用的對象;

  • 方法區中常量引用的對象;

  • 本地方法棧中JNI(Native方法)引用的對象;

  • 活躍線程的引用對象。

三、Java中四種引用

    在JDK1.2之前,Java中的引用定義很單一:如果reference類型的數據中儲存的數值代表的是另一塊內存的起始地址,就稱這塊內存代表着一個引用。但是這種定義太過狹隘,如果某個對象介於被引用和未被引用兩種狀態之間,那麼這種定義就顯得無能爲力。在JDK1.2後Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference),這四種引用強度依次逐漸減弱。

  • 強引用(Strong Reference)

    強引用就是值在程序代碼中普遍存在的,用new關鍵字創建的對象都是強引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象

  • 軟引用(Soft Reference)

    軟引用是用來描述一些還有用但並非必需的對象,在系統將要發生內存溢出之前,將會吧這些對象列入回收範圍之中進行第二次回收。如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。可用來實現高速緩存。軟引用對象在回收時會被放入引用隊列(ReferenceQueue)。

 

 

//  軟引用SoftReference<String> softReference = new SoftReference<>("北風IT之路");
  • 弱引用(Weak Reference)

    弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,**被弱引用關聯的對象只能生存到下一次GC發生之前,當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉該類對象。**弱引用對象在回收時會被放入引用隊列(ReferenceQueue)。

​​​​​​​

//  弱引用WeakReference<String> weakReference = new WeakReference<>("北風IT之路");
  • 虛引用(Phantom Reference)

    虛引用被稱爲幽靈引用或幻象引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得對象實例。任何時候都可能被回收,一般用來跟蹤對象被垃圾收集器回收的活動,起哨兵作用。必須和引用隊列(ReferenceQueue)聯合使用。

​​​​​​​

//  虛引用,必須配合引用隊列使用ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();PhantomReference<String> phantomReference = new PhantomReference<>("北風IT之路",referenceQueue);

四、finalize()賦予對象重生

    在可達性分析算法中被標記爲不可達的對象,也不一定是一定會被回收,它還有第二次重生的機會。每一個對象在被回收之前要進行兩次標記,一次是沒有關聯引用鏈會被標記一次,第二次是判斷該對象是否覆蓋finalize()方法,如果沒有覆蓋則真正的被定了“死刑”。

    如果這個對象被jvm判定爲有必要執行finalize()方法,那麼這個對象會被放入F-Queue隊列中,並在燒燬由一個由虛擬機自動創建的、低優先級的finalizer線程去執行它。但是這裏的“執行”是指虛擬機會觸發這個方法,但是**並不代表會等它運行結束。**虛擬機在此處是做了優化的,因爲如果某個對象在finalize方法中長時間運行或者發送死循環,將可能導致F-Queue隊列中其他對象永遠處於等待,甚至可能會導致整個內存回收系統崩潰。如果要在finalize方法中重生這個對象你可以按照下面代碼做:

​​​​​​​

public class GcTest {    public static GcTest instance = null;
    @Override    protected void finalize() throws Throwable {        super.finalize();        System.out.println("收集器檢測到finalize方法,對象即將獲得一次重生的機會");        instance = this;    }
    public static void main(String[] args) throws InterruptedException{        instance = new GcTest();        //  引用置爲空,堆內對象將視爲垃圾        instance = null;        //  執行gc        System.gc();        Thread.sleep(500);        //  雖然執行了gc,但是可能在finalize方法中獲得重生,        //  因此可能會打印出myObject的地址        System.out.println(instance);        //  最後打印出jvm.GcTest@7cc355be    }}

注意!finalize()方法只會被系統調用一次,多次被gc只有第一次會被調用,因此只有一次的重生機會。

五、回收方法區

    假如一個字符串“abc”已經進入了常量池中,但是當前系統沒有任何一個String對象是“abc”,那麼這個對象就應該回收。方法去(HotSpot虛擬機中的永久代)的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。比如上述的“abc”就是屬於廢棄常量,那麼哪些類是無用的類呢?

  • 該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例;

  • 加載該類的ClassLoader已經被回收;

  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

六、垃圾收集算法

1.標記-清理算法(Mark-Sweep)

 算法思路:算法分爲“標記”和“清理”兩個步驟,首先標記處所有需要回收的對象,在標記完成後再統一回收所有被標記的對象。

缺陷:

  1. 標記和清理的兩個過程效率都不高;

  2. 容易產生內存碎片,碎片空間太多可能導致無法存放大對象。

適用於存活對象佔多數的情況。

圖片來源:https://cloud.tencent.com/developer/article/1336613

2.複製算法(Copy)

算法思路:將可用內存劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊內存用完後,就將還存活的對象複製到另一塊去,然後再把已使用過的內存空間一次清理掉。

缺陷:

  1. 可用內存縮小爲了原來的一半

算法執行效率高,適用於存活對象佔少數的情況。

圖片來源:https://cloud.tencent.com/developer/article/1336613

3.標記-整理算法(Mark-compact)

算法思路:標記過程和標記-清理算法一樣,而後面的不一樣,它是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存

有效地避免了內存碎片的產生。

4.分代收集算法(Generational Collection)

    當前大多數垃圾收集都採用的分代收集算法,這種算法並沒有什麼新的思路,只是根據對象存活週期的不同將內存劃分爲幾塊,每一塊使用不同的上述算法去收集。**在jdk8以前分爲三代:年輕代、老年代、永久代。在jdk8以後取消了永久代的說法,而是元空間取而代之。**一般年輕代使用複製算法(對象存活率低),老年代使用標記整理算法(對象存活率高)。

4.1 年輕代(複製算法爲主)

    儘可能快的收集掉聲明週期短的對象。整個年輕代佔1/3的堆空間,年輕代分爲三個區,Eden、Survivor-from、Survivor-to,其內存大小默認比例爲8:1:1(可調整),大部分新創建的對象都是在Eden區創建。當回收時,先將Eden區存活對象複製到一個Survivor-from區,然後清空Eden區,存活的對象年齡+1;當這個Survivor-from區也存放滿了時,則將Eden區和Survivor-from區存活對象複製到另一個Survivor-to區,然後清空Eden和這個Survivor-from區,存活的對象年齡+1;此時Survivor-from區是空的,然後將Survivor-from區和Survivor-to區交換,即保持Survivor-from區爲空(此時的Survivor-from是原來的Survivor-to區), 如此往復。年輕代執行的GC是Minor GC。

    年輕代的迭代更新很快,大多數對象的存活時間都比較短,所以對GC的效率和性能要求較高,因此使用複製算法,同時這樣劃分爲三個區域,保證了每次GC僅浪費10%的內存,內存利用率也有所提高。

4.2 老年代(標記-整理算法爲主)

    在年輕代經過很多次垃圾回收之後仍然存活的對象(默認15歲),就會被放入老年代中,因爲老年代中的對象大多數是存活的,所以使用算法是標記-整理算法。老年代執行的GC是Full GC。

4.3 永久代/元空間

jdk8以前:

    永久代用於存放靜態文件,如Java類、方法等。該區域回收與上述“方法區內存回收”一致。但是永久代是使用的堆內存,如果創建對象太多容易造成內存溢出OOM(OutOfMemory)。

jdk8以後:

    jdk8以後便取消了永久代的說法,而是用元空間代替,所存內容沒有變化,只是存儲的地址有所改變,元空間使用的是主機內存,而不是堆內存,元空間的大小限制受主機內存限制,這樣有效的避免了創建大量對象時發生內存溢出的情況。

七、Minor GC和Full GC

之前多次提到Minor GC和Full GC,那麼它們有什麼區別呢?

  • Minor GC即新生代GC:發生在新生代的垃圾收集動作,因爲Java有朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。

  • Major GC / Full GC:發生在老年代,經常會伴隨至少一次Minor GC。Major GC的速度一般會比Minor GC慢倍以上。

Minor GC發生條件:

  • 當新對象生成,並且在Eden申請空間失敗時;

Full GC發生條件:

  • 老年代空間不足

  • 永久帶空間不足(jdk8以前)

  • System.gc()被顯示調用

  • Minor GC晉升到老年代的平均大小大於老年代的剩餘空間

  • 使用RMI來進行RPC或管理的JDK應用,每小時執行1次Full GC

八、常見的垃圾收集器(jdk8及以前)

一張圖即可清除看到不同垃圾收集器之間的關係,連線表示可以配合使用。

  • Serial收集器(複製算法)

新生代單線程收集器,標記和清理都是單線程,優點是簡單高效。是client級別默認的GC方式,可以通過-XX:+UseSerialGC來強制指定。

  • Serial Old收集器(標記-整理算法)

老年代單線程收集器,Serial收集器的老年代版本。

  • ParNew收集器(複製算法)

新生代收集器,Serial收集器的多線程版本,在多核CPU情況時表現更好。

  • Parallel Scavenge收集器(複製算法)

並行收集器,追求高吞吐量,高效利用CPU。適合後臺應用等對交互相應要求不高的場景。是server級別默認採用的GC方式,可用-XX:+UseParallelGC來強制指定,用-XX:ParallelGCThreads=2來指定線程數。

  • Parallel Old收集器(複製算法) Parallel Scavenge收集器的老年代版本,並行收集器,吞吐量優先。

  • CMS(Concurrent Mark Sweep)收集器(標記-清理算法) 高併發、低停頓,追求最短GC回收停頓時間(Stop The World),cpu佔用比較高,響應時間快,停頓時間短,多核cpu追求高響應時間的選擇,但是因爲使用標記清理算法,容易產生內存碎片。

  • G1收集器

    G1是一款面向服務端應用的垃圾收集器,支持並行與併發、分代收集、空間整合和可預測停頓的能力,即可適用於年輕代又可適用於老年代。

圖片來源:https://cloud.tencent.com/developer/article/1336613

九、垃圾收集器參數總結

  • UseSerialGC:虛擬機運行在Client模式下的默認值,打開此開關後,使用Serial+Serial Old的收集器組合進行內存回收

  • UseParNewGC:打開此開關後,使用ParNew+Serial Old的收集器組合進行內存回收

  • UseConcMarkSweepGC:打開此開關後,使用ParNew+CMS+Serial Old的收集器組合進行內存回收。Serial Old收集器將作爲CMS收集器出現Concurrent Mode Failure失敗後的後備收集器使用

  • UseParallelGC:虛擬機運行在Server模式下的默認值,打開此開關後,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器組合進行內存回收

  • UseParallelOldGC:打開此開關後,使用Parallel Scavenge + Parallel Old的收集器組合進行內存回收

  • SurvivorRatio:新生代中Eden區域與Survivor區域的容量比值,默認值爲8,代表Eden:Survivor=8:1

  • PretenureSizeThreshold:直接晉升到老年代的對象大小,設置這個參數後,大於這個參數的對象將直接在老年代分配

  • MaxTenuringThreshold:晉升到老年代的對象年齡,每個對象在堅持過一次Minor GC之後,年齡就增加1,當超過這個參數時就進入老年代

  • UseAdaptiveSizePolicy:動態調整Java堆中各個區域的大小以及進入老年代的年齡

  • HandlePromotionFailure:是否允許分配擔保失敗,即老年代的剩餘空間不足以應付新生代的整個Eden和Survivor區的所有對象都存活的極端情況

  • ParallelGCThreads:設置並行GC時進行內存回收的線程數

  • GCTimeRatio:GC時間佔總時間的比率,默認值爲99,即允許1%的GC時間。僅在使用Parallel Scavenge收集器時生效

  • MaxGCPauseMillis:設置GC的最大停頓時間,僅在使用Parallel Scavenge收集器時生效

  • CMSInitingOccupancyFraction:設置CMS收集器在老年代空間被使用多少後觸發垃圾收集。默認值爲68%,僅在使用CMS收集器時生效

  • UseCMSCompactAtFullCollection:設置CMS收集器在完成垃圾收集後是否要進行一次內存碎片整理,僅在使用CMS收集器時生效

  • CMSFullGCsBeforeCompaction:設置CMS收集器在進行若干次垃圾收集後再啓動一次內存碎片整理。僅在使用CMS收集器時生效

參考文獻:《深入理解Java虛擬機》

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