天天看點

《深入了解Java虛拟機》個人讀書總結——垃圾收集/回收算法哪些記憶體需要回收什麼時候回收如何回收

說起垃圾回收,我估計很多初級java開發(包括之前的我)想到的是這個JVM會幫我管理的啊,我們不太需要去考慮這種事情。但是,當需要排查各種記憶體溢出、記憶體洩漏問題時,當垃圾收內建為系統達到更高并發量的瓶頸的時候,我們就有必要對垃圾回收GC進行了解了。思考GC需要完成的3件事情:

1.哪些記憶體需要回收

2.什麼時候回收

3.如何回收

哪些記憶體需要回收

首先我們要考慮的是哪些記憶體需要回收,在介紹Java的記憶體區域的時候,我們知道棧中的棧幀所配置設定的記憶體基本上是在類結構确定下來是就已知的。是以程式計數器、虛拟機棧、本地方法棧這3個區域的記憶體配置設定和回收都具備确定性,它們随着線程而生,随着線程而滅。我們可以認為這一部分記憶體是靜态的。而Java堆和方法區的記憶體配置設定,我們隻有在程式運作時才知道要建立多少對象,進而配置設定多少記憶體。這一部分記憶體我們可以認為是動态的。垃圾收集器所關注的是這部分記憶體。 那回收什麼樣的記憶體?當然是回收那些建立出來的對象都沒人用的對象所占用的記憶體空間,是以在對這部分記憶體回收之前,第一件事就是需要确認這些對象是死是活(不可能再被任何途徑使用的對象即認為是死)

引用計數算法

引用計數法的基本思想是:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1,;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。

客觀地說,這個判斷方法實作簡單,判定效率也很高,也有些應用案例,但在主流的Java虛拟機裡并沒有選擇此方法來管理記憶體,其中最主要的原因是它很難解決對象之間循環引用的問題。

可達性分析算法

目前主流的Java虛拟機都采用可達性分析來判定對象是否存活的。這個算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鍊,當一個對象到GC Roots沒有任何引用鍊相連時,則證明此對象是不可用的。如圖所示:

《深入了解Java虛拟機》個人讀書總結——垃圾收集/回收算法哪些記憶體需要回收什麼時候回收如何回收

對象5.6.7雖然它們之間有關聯,但是它們與GC Roots是不可達的,是以判定它們是可回收的對象。

在Java語言中,可作為GC Roots的對象包括下面幾種:

1.虛拟機棧(棧幀中的本地變量表)中引用的對象。

2.方法區中類靜态屬性引用的對象。

3.方法區中常量引用的對象。

4.本地方法棧中JNI(即一般說的Native方法)引用的對象。

總結上面所說,都牽扯到一個詞“引用”。在JDK1.2之前,對引用的定義就是說如果reference類型的資料總存儲的數值代表的是另外一塊記憶體的起始位址,就稱這塊記憶體代表着一個引用。但這樣的定義使得我們的對象就隻有被引用和沒有被引用這兩種狀态。然而我們還是想要說想表示更多的狀态,如當記憶體還夠時我們還不想删除的那些對象。在JDK1.2之後。Java對引用的概念進行了擴充,将引用分為強引用、軟引用、弱引用、虛引用4種,引用強度依次減弱。

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

2.軟引用也是用來描述一些有用但并非必要的對象。對于軟引用關聯着的對象,在系統将要發生記憶體溢出異常之前,将會把這些對象列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的記憶體,才會抛出記憶體溢出異常。

3.弱引用也是用來描述非必要對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象隻能生存到下一次垃圾收集發生之前。但垃圾收集工作時,無論目前記憶體是否足夠,都會回收掉隻内弱引用關聯的對象。

4.虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象執行個體。為一個對象設定虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。在JDK1.2之後,提供了PhantomReference類來實作虛引用。

要不要馬上“死”?

盡管通過以上的判定方法可以找到不可達的對象,但是要真正宣告一個對象死亡,至少要經曆兩次标記過程:第一次标記是在可達性分析後發現沒有與GC Roots相連接配接的引用鍊并同時進行一次篩選,如果篩選出此對象沒有必要執行finalize方法(目前對象沒有覆寫finalize方法或此方法已經被執行過了,注意:finalize方法在整個對象生命周期隻會被調用一次而已)。沒有必要執行finalize方法的對象确認已經死亡。如果有必要執行finalize方法的對象則會将此對象放入一個叫做F-Queue的隊列之中,并稍後有一個虛拟機自動建立的、低優先級的Finalizer線程去執行它。這也是對象最後的自救時刻,它可在finalize方法把自己(this)指派給某個類變量或者對象的成員變量來自救不在這次垃圾回收掉,這是第二次标記。如果第二次标記不成功,那就真的回收了。

方法區回收

上面說的方法都是在堆記憶體中進行記憶體管理的。方法區的記憶體,即我們常說的永久代(在Java8裡也已經沒有永久代這個概念了)。雖然在Java虛拟機規範中并沒有要求在方法區也要實作垃圾收集。的确,在這裡收集記憶體的效率太低1了。在這部分記憶體要回收的主要是廢棄常量和無用的類。常量是不是廢棄的很好判斷,但類要同時滿足以下3個條件才能算是“無用的類”:

1.該類的所有執行個體都已經被回收,也就是Java堆裡面不存在該類的任何執行個體。

2.加載該類的ClassLoader已經被回收。

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

在這裡,對類的回收不像對象一樣,沒用了就回收,類回不回收HotSpot虛拟機提供了-Xnoclassgc參數進行控制。

什麼時候回收

我們之前說過,從記憶體回收的角度來看,由于現在的收集器基本都采用分代收集算法,是以Java堆中可以被細分為:新生代和老年代;再細緻一點的有Eden空間、From Survivor空間、To Survivor空間等。 年輕代是所有新對象産生的地方。當年輕代記憶體空間被用完時,這時就會觸發垃圾回收。這個垃圾回收叫做Minor GC。

老年代記憶體裡包含了長期存活的對象和經過多次Minor GC後依然存活下來的對象。通常會在老年代記憶體被占滿時進行垃圾回收。老年代的垃圾收集叫做Major GC。這裡注意,發生在老年代的垃圾收集也有人叫Full GC,這兩個術語目前還沒有正式的定義。但我更傾向于說永久代的垃圾收集是Full GC。

如何回收

垃圾收集算法幫我們更有效率地回收記憶體

标記-清除算法

最基礎的收集算法是“标記-清除”算法,算法分為“标記”和“清除”兩個階段:首先标記出所需要回收的對象,在标記完成後統一回收所有被标記的對象。之是以說它是最基礎的收集算法,是因為後續的收集算法都是基于這種思想并對其不足之處進行改進而已。它的主要不足之處有兩個:一個是效率問題,标記和清除兩個過程的效率都不高;另一個是空間問題,标記清除後悔産生大量的不連續的記憶體碎片,空間碎片太多可能會導緻以後在程式運作過程中需要配置設定較大對象是,無法找到足夠的連續記憶體而不得不提前出發另一次垃圾收集動作。“标記-清除”算法的執行過程如圖所示

《深入了解Java虛拟機》個人讀書總結——垃圾收集/回收算法哪些記憶體需要回收什麼時候回收如何回收

複制算法

為了解決效率問題,可以将記憶體按容量大小劃分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊的記憶體用完了,就将還存活着的對象複制到另外一塊上面,然後再把已經使用過的記憶體空間一次性清理掉,這就是“複制”算法。在這裡還要說說新生代的記憶體空間比例,IBM的研究表明新生代中有98%的對象都是短命的,是以并不需要1:1來劃分記憶體空間,而是将記憶體分為較大的一塊Eden空間和兩塊較小的Survivor空間(HotSpot虛拟機比例是8:1),每次使用Eden和其中一塊Survivor,留一塊Survivor用來複制兩個空間中還存活的對象,然後直接清空那兩個空間。還有就是我們還不能保證每次那個剩下的那個Survivor空間都有足夠的空間複制存活的對象,是以還需要依賴其他記憶體(這裡指老年代)進行配置設定擔保,如果空間不足,這些對象将通過配置設定擔保機制進入老年代。“複制”算法的執行過程如圖所示

《深入了解Java虛拟機》個人讀書總結——垃圾收集/回收算法哪些記憶體需要回收什麼時候回收如何回收

标記-整理算法

複制算法在對象存活率較高時就要進行較多的複制操作,效率将會變低。特别在老年代,老年代一般不能直接選用這種算法,是以有人提出了另外一種“标記-整理”算法,标記過程仍然和“标記-清除”算法一樣,但後續步驟不是直接對可回收對象進行整理,而是讓所有存活的對象向一端移動,然後直接清理掉邊界以外的記憶體。“标記-整理”算法的執行過程如圖所示

《深入了解Java虛拟機》個人讀書總結——垃圾收集/回收算法哪些記憶體需要回收什麼時候回收如何回收

分代收集除算法

目前商業虛拟機的垃圾收集都采用“分代收集”算法。一般将Java堆分成新生代和老年代,然後根絕各個年代的特點采用适當的手機算法。在新生代采用“複制”算法,在老年代采用“标記-清理”或“标記-整理”算法。

緻謝:以上的配圖都來自百度圖檔,感謝原作者。