圖解Java 垃圾回收機制

摘要:

Java技術體系中所提倡的 自動內存管理 最終可以歸結爲自動化地解決了兩個問題:給對象分配內存 以及 回收分配給對象的內存,而且這兩個問題針對的內存區域就是Java內存模型中的 堆區。關於對象分配內存問題,筆者的博文《JVM 內存模型概述》已經闡述了 如何劃分可用空間及其涉及到的線程安全問題,本文將結合垃圾回收策略進一步給出內存分配規則。垃圾回收機制的引入可以有效的防止內存泄露、保證內存的有效使用,也大大解放了Java程序員的雙手,使得他們在編寫程序的時候不再需要考慮內存管理。本文着重介紹了判斷一個對象是否可以被回收的兩種經典算法,並詳述了四種典型的垃圾回收算法的基本思想及其直接應用——垃圾收集器,最後結合內存回收策略介紹了內存分配規則。


版權聲明:

本文原創作者:書呆子Rico
作者博客地址:http://blog.csdn.net/justloveyou_/

本文涉及到的所有圖例均由筆者整理所得,其中部分由筆者親自繪製,部分借鑑於互聯網並在其基礎上修改而成。若涉及版權,請留言評論或直接聯繫筆者,聯繫方式見左側欄。


友情提示:

爲了更好地瞭解Java的垃圾回收機制,筆者建議讀者先要對JVM內存模型有一個整體的瞭解和把握。鑑於筆者在博文《JVM 內存模型概述》中已經深入介紹了JVM內存模型,此不贅述。

  本文內容是基於 JDK 1.6 的,不同版本虛擬機之間也許會有些許差異,但不影響我們對JVM垃圾回收機制的整體把握和了解。

一、垃圾回收機制的意義

在筆者的上一篇博文《JVM 內存模型概述》中提到,JVM 內存模型一共包括三個部分:堆 ( Java代碼可及的 Java堆 和 JVM自身使用的方法區)、棧 ( 服務Java方法的虛擬機棧 和 服務Native方法的本地方法棧 ) 和 保證程序在多線程環境下能夠連續執行的程序計數器。特別地,我們當時就提到Java堆是進行垃圾回收的主要區域,故其也被稱爲GC堆;而方法區也有一個不太嚴謹的表述,就是永久代。總的來說,堆 (包括Java堆 和 方法區)是垃圾回收的主要對象,特別是Java堆。

實際上,Java技術體系中所提倡的自動內存管理 最終可以歸結爲自動化地解決了兩個問題:給對象分配內存 以及回收分配給對象的內存,而且這兩個問題針對的內存區域就是Java內存模型中的堆區。關於對象分配內存問題,筆者的博文《JVM 內存模型概述》已經闡述了 如何劃分可用空間及其涉及到的線程安全問題,本文將結合垃圾回收策略進一步給出 內存分配規則。另外,我們知道垃圾回收機制垃圾回收機制垃圾回收機制是Java語言一個顯著的特點,其可以有效的防止內存泄露、保證內存的有效使用,從而使得Java程序員在編寫程序的時候不再需要考慮內存管理問題。Java 垃圾回收機制要考慮的問題很複雜,本文闡述了其三個核心問題,包括:

  • 那些內存需要回收?(對象是否可以被回收的兩種經典算法: 引用計數法 和 可達性分析算法)
  • 什麼時候回收? (堆的新生代、老年代、永久代的垃圾回收時機,MinorGC 和 FullGC)
  • 如何回收?(三種經典垃圾回收算法(標記清除算法、複製算法、標記整理算法)及分代收集算法 和 七種垃圾收集器)

在探討Java垃圾回收機制之前,我們首先應該記住一個單詞:Stop-the-World。Stop-the-world意味着 JVM由於要執行GC而停止了應用程序的執行,並且這種情形會在任何一種GC算法中發生。當Stop-the-world發生時,除了GC所需的線程以外,所有線程都處於等待狀態直到GC任務完成。事實上,GC優化很多時候就是指減少Stop-the-world發生的時間,從而使系統具有 高吞吐 、低停頓 的特點。

  Ps: 內存泄露是指該內存空間使用完畢之後未回收,在不涉及複雜數據結構的一般情況下,Java 的內存泄露表現爲一個內存對象的生命週期超出了程序需要它的時間長度。


二. 如何確定一個對象是否可以被回收?

1、 引用計數算法:判斷對象的引用數量

引用計數算法是通過判斷對象的引用數量來決定對象是否可以被回收。

引用計數算法是垃圾收集器中的早期策略。在這種方法中,堆中的每個對象實例都有一個引用計數。當一個對象被創建時,且將該對象實例分配給一個引用變量,該對象實例的引用計數設置爲 1。當任何其它變量被賦值爲這個對象的引用時,對象實例的引用計數加 1(a = b,則b引用的對象實例的計數器加 1),但當一個對象實例的某個引用超過了生命週期或者被設置爲一個新值時,對象實例的引用計數減 1。特別地,當一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數器均減 1。任何引用計數爲0的對象實例可以被當作垃圾收集。

  引用計數收集器可以很快的執行,並且交織在程序運行中,對程序需要不被長時間打斷的實時環境比較有利,但其很難解決對象之間相互循環引用的問題。如下面的程序和示意圖所示,對象objA和objB之間的引用計數永遠不可能爲 0,那麼這兩個對象就永遠不能被回收。

這裏寫圖片描述

  public class ReferenceCountingGC {
  
        public Object instance = null;

        public static void testGC(){

            ReferenceCountingGC objA = new ReferenceCountingGC ();
            ReferenceCountingGC objB = new ReferenceCountingGC ();

            // 對象之間相互循環引用,對象objA和objB之間的引用計數永遠不可能爲 0
            objB.instance = objA;
            objA.instance = objB;

            objA = null;
            objB = null;

            System.gc();
    }
}

上述代碼最後面兩句將objA和objB賦值爲null,也就是說objA和objB指向的對象已經不可能再被訪問,但是由於它們互相引用對方,導致它們的引用計數器都不爲 0,那麼垃圾收集器就永遠不會回收它們。


2、 可達性分析算法:判斷對象的引用鏈是否可達

  可達性分析算法是通過判斷對象的引用鏈是否可達來決定對象是否可以被回收。

  可達性分析算法是從離散數學中的圖論引入的,程序把所有的引用關係看作一張圖,通過一系列的名爲 “GC Roots” 的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain)。當一個對象到 GC Roots 沒有任何引用鏈相連(用圖論的話來說就是從 GC Roots 到這個對象不可達)時,則證明此對象是不可用的,如下圖所示。在Java中,可作爲 GC Root 的對象包括以下幾種:

虛擬機棧(棧幀中的局部變量表)中引用的對象;

  • 方法區中類靜態屬性引用的對象;
  • 方法區中常量引用的對象;
  • 本地方法棧中Native方法引用的對象;

可達性分析算法示意圖.jpg-101.8kB

三. 垃圾收集算法

1、標記清除算法

標記-清除算法分爲標記和清除兩個階段。該算法首先從根集合進行掃描,對存活的對象標記,標記完畢後,再掃描整個空間中未被標記的對象並進行回收,如下圖所示。

可達性分析算法.jpg-26.7kB

標記-清除算法的主要不足有兩個:

  • 效率問題:標記和清除兩個過程的效率都不高;
  • 空間問題:標記-清除算法不需要進行對象的移動,並且僅對不存活的對象進行處理,因此標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

標記-清除算法-10.6kB


2、複製算法

複製算法將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。這種算法適用於對象存活率低的場景,比如新生代。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。該算法示意圖如下所示:

複製算法-5.7kB

事實上,現在商用的虛擬機都採用這種算法來回收新生代。因爲研究發現,新生代中的對象每次回收都基本上只有10%左右的對象存活,所以需要複製的對象很少,效率還不錯。正如在博文《JVM 內存模型概述》中介紹的那樣,實踐中會將新生代內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間 (如下圖所示),每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活着的對象一次地複製到另外一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是 8:1,也就是每次新生代中可用內存空間爲整個新生代容量的90% ( 80%+10% ),只有10% 的內存會被“浪費”。

heap.bmp-174kB


3、標記整理算法

複製收集算法在對象存活率較高時就要進行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。標記整理算法的標記過程類似標記清除算法,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存,類似於磁盤整理的過程,該垃圾回收算法適用於對象存活率高的場景(老年代),其作用原理如下圖所示:

標記-整理算法1.jpg-33.6kB

標記整理算法與標記清除算法最顯著的區別是:標記清除算法不進行對象的移動,並且僅對不存活的對象進行處理;而標記整理算法會將所有的存活對象移動到一端,並對不存活對象進行處理,因此其不會產生內存碎片。標記整理算法的作用示意圖如下:

標記整理算法示意圖.png-14.2kB


4、分代收集算法

對於一個大型的系統,當創建的對象和方法變量比較多時,堆內存中的對象也會比較多,如果逐一分析對象是否該回收,那麼勢必造成效率低下。分代收集算法是基於這樣一個事實:不同的對象的生命週期(存活情況)是不一樣的,而不同生命週期的對象位於堆中不同的區域,因此對堆內存不同區域採用不同的策略進行回收可以提高 JVM 的執行效率當代商用虛擬機使用的都是分代收集算法新生代對象存活率低,就採用複製算法;老年代存活率高,就用標記清除算法或者標記整理算法。Java堆內存一般可以分爲新生代、老年代和永久代三個模塊,如下圖所示:
 

分代收集算法總.jpg-35.5kB


1). 新生代(Young Generation)

  新生代的目標就是儘可能快速的收集掉那些生命週期短的對象,一般情況下,所有新生成的對象首先都是放在新生代的。新生代內存按照 8:1:1 的比例分爲一個eden區和兩個survivor(survivor0,survivor1)區,大部分對象在Eden區中生成。在進行垃圾回收時,先將eden區存活對象複製到survivor0區,然後清空eden區,當這個survivor0區也滿了時,則將eden區和survivor0區存活對象複製到survivor1區,然後清空eden和這個survivor0區,此時survivor0區是空的,然後交換survivor0區和survivor1區的角色(即下次垃圾回收時會掃描Eden區和survivor1區),即保持survivor0區爲空,如此往復。特別地,當survivor1區也不足以存放eden區和survivor0區的存活對象時,就將存活對象直接存放到老年代。如果老年代也滿了,就會觸發一次FullGC,也就是新生代、老年代都進行回收。注意,新生代發生的GC也叫做MinorGC,MinorGC發生頻率比較高,不一定等 Eden區滿了才觸發。


2). 老年代(Old Generation)

  老年代存放的都是一些生命週期較長的對象,就像上面所敘述的那樣,在新生代中經歷了N次垃圾回收後仍然存活的對象就會被放到老年代中。此外,老年代的內存也比新生代大很多(大概比例是1:2),當老年代滿時會觸發Major GC(Full GC),老年代對象存活時間比較長,因此FullGC發生的頻率比較低。


3). 永久代(Permanent Generation)

  永久代主要用於存放靜態文件,如Java類、方法等。永久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些class,例如使用反射、動態代理、CGLib等bytecode框架時,在這種時候需要設置一個比較大的永久代空間來存放這些運行過程中新增的類。


5、小結

這裏寫圖片描述


由於對象進行了分代處理,因此垃圾回收區域、時間也不一樣。垃圾回收有兩種類型,Minor GCFull GC

  • Minor GC:對新生代進行回收,不會影響到年老代。因爲新生代的 Java 對象大多死亡頻繁,所以 Minor GC 非常頻繁,一般在這裏使用速度快、效率高的算法,使垃圾回收能儘快完成。
  • Full GC:也叫 Major GC,對整個堆進行回收,包括新生代老年代。由於Full GC需要對整個堆進行回收,所以比Minor GC,因此應該儘可能減少Full GC的次數,導致Full GC的原因包括:老年代被寫滿、永久代(Perm)被寫滿和System.gc()被顯式調用等

    四. 垃圾收集器

如果說垃圾收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。下圖展示了7種作用於不同分代的收集器,其中用於回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,還有用於回收整個Java堆的G1收集器。不同收集器之間的連線表示它們可以搭配使用。

垃圾收集器.jpg-64.2kB

  • Serial收集器複製算法): 新生代單線程收集器,標記和清理都是單線程,優點是簡單高效;
  • Serial Old收集器 (標記-整理算法): 老年代單線程收集器,Serial收集器的老年代版本;
  • ParNew收集器 (複製算法): 新生代收並行集器,實際上是Serial收集器的多線程版本,在多核CPU環境下有着比Serial更好的表現;
  • Parallel Scavenge收集器 (複製算法): 新生代並行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用戶線程時間/(用戶線程時間+GC線程時間),高吞吐量可以高效率的利用CPU時間,儘快完成程序的運算任務,適合後臺應用等對交互相應要求不高的場景;
  • Parallel Old收集器 (標記-整理算法): 老年代並行收集器,吞吐量優先,Parallel Scavenge收集器的老年代版本;
  • CMS(Concurrent Mark Sweep)收集器(標記-清除算法): 老年代並行收集器,以獲取最短回收停頓時間爲目標的收集器,具有高併發、低停頓的特點,追求最短GC回收停頓時間。
  • G1(Garbage First)收集器 (標記-整理算法): Java堆並行收集器,G1收集器是JDK1.7提供的一個新收集器,G1收集器基於“標記-整理”算法實現,也就是說不會產生內存碎片。此外,G1收集器不同於之前的收集器的一個重要特點是:G1回收的範圍是整個Java堆(包括新生代,老年代),而前六種收集器回收的範圍僅限於新生代或老年代。

五. 內存分配與回收策略

Java技術體系中所提倡的自動內存管理最終可以歸結爲自動化地解決了兩個問題:給對象分配內存 以及 回收分配給對象的內存。一般而言,對象主要分配在新生代的Eden區上,如果啓動了本地線程分配緩存(TLAB),將按線程優先在TLAB上分配。少數情況下也可能直接分配在老年代中。總的來說,內存分配規則並不是一層不變的,其細節取決於當前使用的是哪一種垃圾收集器組合,還有虛擬機中與內存相關的參數的設置。

1) 對象優先在Eden分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次MinorGC。現在的商業虛擬機一般都採用複製算法來回收新生代,將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。 當進行垃圾回收時,將Eden和Survivor中還存活的對象一次性地複製到另外一塊Survivor空間上,最後處理掉Eden和剛纔的Survivor空間。(HotSpot虛擬機默認Eden和Survivor的大小比例是8:1)當Survivor空間不夠用時,需要依賴老年代進行分配擔保。

  2) 大對象直接進入老年代。所謂的大對象是指,需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組。

  3) 長期存活的對象將進入老年代。當對象在新生代中經歷過一定次數(默認爲15)的Minor GC後,就會被晉升到老年代中。

  4) 動態對象年齡判定。爲了更好地適應不同程序的內存狀況,虛擬機並不是永遠地要求對象年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

  需要注意的是,Java的垃圾回收機制是Java虛擬機提供的能力,用於在空閒時間以不定時的方式動態回收無任何引用的對象佔據的內存空間。也就是說,垃圾收集器回收的是無任何引用的對象佔據的內存空間而不是對象本身。


六. Java中的內存泄露問題

雖然Java擁有垃圾回收機制,但同樣會出現內存泄露問題,比如下面提到的幾種情況:

(1). 諸如 HashMap、Vector 等集合類的靜態使用最容易出現內存泄露,因爲這些靜態變量的生命週期和應用程序一致,所有的對象Object也不能被釋放,因爲他們也將一直被Vector等應用着。


private static Vector v = new Vector(); 

public void test(Vector v){

    for (int i = 1; i<100; i++) { 
        Object o = new Object(); 
        v.add(o); 
        o = null; 
    }
}

在這個例子中,虛擬機棧中保存者 Vector 對象的引用 v 和 Object 對象的引用 o 。在 for 循環中,我們不斷的生成新的對象,然後將其添加到 Vector 對象中,之後將 o 引用置空。問題是雖然我們將 o 引用置空,但當發生垃圾回收時,我們創建的 Object 對象也不能夠被回收。因爲垃圾回收在跟蹤代碼棧中的引用時會發現 v 引用,而繼續往下跟蹤就會發現 v 引用指向的內存空間中又存在指向 Object 對象的引用。也就是說,儘管o 引用已經被置空,但是 Object 對象仍然存在其他的引用,是可以被訪問到的,所以 GC 無法將其釋放掉。如果在此循環之後, Object 對象對程序已經沒有任何作用,那麼我們就認爲此 Java 程序發生了內存泄漏。


(2). 各種資源連接包括數據庫連接、網絡連接、IO連接等沒有顯式調用close關閉,不被GC回收導致內存泄露。


(3). 監聽器的使用,在釋放對象的同時沒有相應刪除監聽器的時候也可能導致內存泄露。


七. 知識點補充

1、引用

1). 引用概述

無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象的引用鏈是否可達,判定對象是否存活都與“引用”有關。在JDK 1.2之前,Java中的引用的定義很傳統:如果reference類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就稱這塊內存代表着一個引用。這種定義很純粹,但是太過狹隘,一個對象在這種定義下只有被引用或者沒有被引用兩種狀態,對於如何描述一些“食之無味,棄之可惜”的對象就顯得無能爲力。我們希望能描述這樣一類對象:當內存空間還足夠時,則能保留在內存之中;如果內存在進行垃圾收集後還是非常緊張,則可以拋棄這些對象。很多系統的緩存功能都符合這樣的應用場景。

爲此,在JDK 1.2之後,Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,這四種引用強度依次逐漸減弱。


 2). 引用的種類及其定義

 

強引用就是指在程序代碼之中普遍存在的,類似“Object obj = new Object()”這類引用。 只要強引用還存在,垃圾收集器就永遠不會回收掉被引用的對象。

  軟引用用來描述一些還有用,但並非必需的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中並進行第二次回收。如果這次回收還是沒有足夠的內存,纔會拋出內存溢出異常。在JDK 1.2之後,提供了SoftReference類來實現軟引用。

  弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK 1.2之後,提供了WeakReference類來實現弱引用。

  虛引用是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的唯一目的就是希望能在這個對象被收集器回收時收到一個系統通知。在JDK 1.2之後,提供了PhantomReference類來實現虛引用。


2、方法區的回收

方法區的內存回收目標主要是針對 常量池的回收對類型的卸載。回收廢棄常量與回收Java堆中的對象非常類似。以常量池中字面量的回收爲例,假如一個字符串“abc”已經進入了常量池中,但是當前系統沒有任何一個String對象是叫做“abc”的,換句話說是沒有任何String對象引用常量池中的“abc”常量,也沒有其他地方引用了這個字面量,如果在這時候發生內存回收,而且必要的話,這個“abc”常量就會被系統“請”出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似。

  判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是“無用的類”:

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

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

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

虛擬機可以對滿足上述3個條件的無用類進行回收(卸載),這裏說的僅僅是“可以”,而不是和對象一樣,不使用了就必然會回收。特別地,在大量使用反射、動態代理、CGLib等bytecode框架的場景,以及動態生成JSP和OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。


八. 更多

 更多關於JVM內存模型的結構、Java對象在虛擬機中的創建、定位過程、內存異常分析等相關知識的介紹,請各位看官移步我的博文請移步我的博文《JVM 內存模型概述》

更多關於 Java SE 進階 方面的內容,請關注我的專欄 《Java SE 進階之路》。本專欄主要研究 JVM基礎、Java源碼和設計模式等Java進階知識,從初級到高級不斷總結、剖析各知識點的內在邏輯,貫穿、覆蓋整個Java知識面,在一步步完善、提高把自己的同時,把對Java的所學所思分享給大家。萬丈高樓平地起,基礎決定你的上限,讓我們攜手一起勇攀Java之巔…

引用:

《深入理解Java虛擬機》 第二版 周志明
深入理解 Java 垃圾回收機制


原文鏈接:https://blog.csdn.net/justloveyou_/article/details/71216049


 

 

 

 

 


 

 

 

 

 


 

發佈了572 篇原創文章 · 獲贊 273 · 訪問量 93萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章