JVM之垃圾回收過程

JVM之垃圾回收過程

本文主要學習自《深刻理解Java虛擬機》,內容順序大體遵循書本順序,加上自身對於其中算法的理解學習。今天小泉主要跟你們一塊兒學習一下垃圾回收的整個流程。java

前言

垃圾回收(Garbage Collection),是一種自動內存管理機制。 按照慣例,下面放出垃圾回收的維基百科定義。程序員

維基百科
在計算機科學中,垃圾回收(英語:Garbage Collection,縮寫爲GC)是指一種自動的存儲器管理機制。
當某個程序佔用的一部份內存空間再也不被這個程序訪問時,這個程序會藉助垃圾回收算法向操做系統歸還這部份內存空間。垃圾回收器能夠減輕程序員的負擔,也減小程序中的錯誤。算法

GC比Java誕生的更早,最先起源於Lisp語言,1960年MIT的大佬們就開始研究如何可以自動管理內存和垃圾回收技術了。通過這麼多年的發展,JVM中的GC機制已經十分健壯與成熟了。緩存

斷定垃圾回收的對象

回收垃圾以前,首先要找到須要被看成垃圾而回收的對象。markdown

JVM分爲五個區域——程序計數器、虛擬機棧、本地方法棧、堆、方法區。咱們知道程序計數器與棧均是線程私有的,其生命週期取決於線程的生命週期。所以不須要考慮其中回收的問題。數據結構

JVM

而堆和方法區屬於線程共享部分,是對象實例、變量存放的地方,是可以被動態分配的。所以根據前文定義以及此處的分析,咱們能夠知道,須要被看成垃圾被回收的是堆和方法區中沒法再次被訪問的內存區域。函數

堆的回收

那麼知道了垃圾回收的對象的定義,那麼應當如何判斷當前對象是否沒法再次被訪問即對象「死亡」呢?post

引用計數算法(Reference Counting)

引用計數算法相對來講比較簡單,對象實例化後該實例對象引用計數初始值爲1,後續根據該實例對象的引用與釋放來進行增一、減1操做,當這一實例對象的計數器歸零,則斷定該對象「死亡」。學習

引用計數算法原來相對簡單,斷定效率相對來講也就高一些,許多地方都採用了引用計數算法,如微軟的COM(Component Object Model)技術、Python語言、Object-C語言都採用了引用計數算法的內存管理方式。優化

固然引用計數算法也有着本身的弊端,好比對於對象之間的相互引用。舉個書中給出的小栗子:

public clss ReferenceCountingGC {
  public Object instance = null;
  private int size = 10;//無實際意義
  
  public statice void testGC() {
    ReferenceCountingGC objA = new ReferenceCountingGC();
    ReferenceCountingGC objB = new ReferenceCountingGC();
    //相互引用
    objA.instance = objB;
    objB.instance = objA;
    
    objA = null;
    objB = null;
  }
}
複製代碼

對象實例化計數器初始值爲1,被引用+1,最後引用釋放-1,最後兩個對象的計數器值爲1,對象未死亡,可在其餘地方這兩個對象再無使用,也就是該內存不可以被回收。

可達性分析算法(Reachability Analysis)

可達性分析算法是目前C#和Java主用的斷定算法。其算法流程以下:

  1. 選定根對象(GC Roots)做爲起始節點集
  2. 從起始節點開始,根據對象引用關係進行向下繼續搜索節點,造成引用鏈

從數據結構與算法的角度重構這一過程:

以實例對象爲節點,以對象的引用關係爲以邊造成的無向圖,從無向圖中選取特定的節點做爲特定節點,選出與這些特定節點沒有任何關聯(直接或者間接能夠訪問到)的節點。

其實就是圖論中無向圖的遍歷,若是隻是從算法角度去考慮,這裏能夠考慮採用構建鄰接矩陣或者鄰接鏈表並使用DFS或者BFS的方式去處理,這個會在之後數據結構與算法系列中推出。

Reachability Analysis

如圖,從GC Roots開始搜索到的節點均是存活對象,而未搜索到的節點則會被斷定爲「死亡」對象。

知道了算法流程,還須要知道一個前提,咱們如何選取特定節點也就是根對象?

在Java中,書中給出幾種GC Roots的固定來源:

  • 虛擬機棧中引用的對象——臨時變量、局部變量等
  • 方法區中類靜態屬性引用的對象——靜態變量等
  • 方法區中常量引用的對象——字符串常量池等
  • 本地方法棧JNI(Native方法)引用的對象
  • JVM內部引用——基礎數據類型、系統類加載器、常見異常對象(OOM等)等
  • 同步鎖(sychronized關鍵字)持有的對象
  • 反映Java虛擬機內狀況的JMXBean、本地代碼緩存等等

除了固定GC Roots集合,還有一些「臨時性」對象會被加入到該集合中,共同構成GC Roots集合。這一部分在之後再繼續講解吧。

那麼是否是隻要與GC Roots集合無關聯即不可達對象就會被回收呢?

不可達就要被回收了嗎?

不可達還不意味着必定就會被回收。JVM在通過可達性分析算法斷定某一對象與GC Roots無關後,會對這一對象作一個標記,後續會進行二次斷定,而二次斷定的內容是:是否有必要實行對象的finalize(),若是對象沒有覆蓋finalize()或者finalize()方法已經被虛擬機調用了,那麼便可認定爲「沒有必要執行」。

若是二次斷定爲「有必要執行」,那麼對象會被放置在F-Queue的隊列中,以後會有一條由JVM自動創建且低調度優先級的Finalizer線程去執行隊列中對象的finalize(),執行機位觸發方法而不等待方法執行結束

所以,不可達對象在此時還有着拯救本身的方法,那就是重寫finalize()函數,但注意finalize()函數也只會進入一次,也就是不可達對象僅有一次拯救本身的辦法。

方法區的回收

對於方法區的回收,在前文介紹JVM內存區域已經說起到了。

方法區回收

針對回收的對象,方法區回收主要分兩類,一類是常量池中的廢棄常量,一類是再也不使用的類型。

廢棄常量

廢棄常量的斷定和堆中對象的斷定十分相近,再也不贅述,可參考對回收中的引用計數。

再也不使用的類型

一樣,針對「再也不使用」,須要規定其範圍。

  1. 該類型(包括派生子類)全部實例已經被回收
  2. 該類型的類加載器已經被回收(難以達成)
  3. 其java.lang.Class對象沒有被引用

達到上述三個要求的類型纔可以容許被當垃圾被回收,然而這裏須要注意的是,此處也只是「容許」而非必然。

垃圾收集

分代收集理論

在肯定了垃圾回收的對象,那麼還須要有着回收垃圾的管理者,也就是垃圾收集器,目前大多數垃圾收集器遵循着分代收集理論,該理論基於兩個假說提出:

  1. 弱分代假說(Weak Generational Hypothesis):絕大多數對象都是朝生夕滅的。
  2. 強分代假說(Strong Geneartional Hypothesis):熬過越屢次垃圾收集過程的對象就越難以消亡。

基於以上兩個假說,咱們的垃圾收集器會將Java堆劃分爲多個不一樣區域,同時將回收對象依據對象熬過垃圾收集的次數劃分到各個區域,回收垃圾則在回收時會對某一個或者某幾個區域進行回收。

雖然不一樣的垃圾收集器劃分的區域不一樣,但區域大體會劃分爲新生代(Young Generaion)和老年代(Old Generation)兩個區域。正是因爲這樣的劃分也致使了一個問題——兩個區域的對象有着引用關係(稱之爲跨代引用),這樣就對某個區域的回收形成了影響(必須再去查詢區域中的存活對象)。

爲此引入了第三點假說:

  1. 跨代引用假說(Intergenerational Reference Hypothesis):跨代引用相對於同代引用來講僅佔極少數。

有了第三點假說,咱們就能夠認爲當發生新生代和老年代的跨代引用,老年代的難以消亡特色會使得引用的新生代對象得以存活並隨着垃圾收集次數的增長而晉升到老年代。

同時第三點假說告訴咱們跨代引用的部分會不多不多,所以咱們只須要創建一種數據結構用來標記住老年代中進行了跨代引用的內存區域,將其加入到GC Roots中進行新生代存活對象的斷定。

垃圾收集種類

根據垃圾收集的區域不一樣,主要分爲如下幾種垃圾回收的方式:

  1. Partial GC:局部垃圾收集,指的是目標不是整個Java堆扽的垃圾收集
    • Minor GC :新生代收集,目標僅針對新生代
    • Major GC :老年代收集,目標僅針對老年代
    • Mixed GC :混合收集,目標是收集新生代以及部分老年代
  2. Full GC :整堆收集,目標是整個Java堆和方法區

垃圾收集器

垃圾收集器這裏先不作介紹,等深刻學習後再跟你們分享。

垃圾收集算法

根據斷定回收垃圾對象的方式,垃圾回收的算法也分爲兩種,一種稱爲「引用計數式垃圾回收」(ReferenceCounting GC),另外一種稱爲「追蹤式垃圾回收」(Tracing GC)。因爲JVM採用的是可達性分析算法進行「垃圾」的斷定,所以一下主要介紹後一種收集算法。

標記-清除算法(Mark-Sweep)

標記清除算法是垃圾收集算法的基礎。它的思路相對來講是最簡單的,按字面意思理解,先「標記」,再「清除」:

  1. 標記

    首先按照可達性分析算法所述,先斷定可達性再進行二次斷定後標記爲可回收

  2. 清除

    將標記爲可回收的區域清除回收

標記-清除算法

標記-清除的過程很簡單,所以也有着許多缺點,主要可歸爲兩點:

  1. 效率較低

    標記和清除的過程主要在於掃描對象:先掃描全部對象進行標記,再掃描判斷標記進而清除,所以當可回收對象的數量增多時其效率就會下降。

  2. 碎片化內存

    內存碎片化是可想而知的,可回收的對象不必定會保存在連續一段的內存空間,所以不經任何處理直接清除,會形成諸多的碎片化空間,類比於操做系統的內存管理操做。

標記-複製算法(Mark-Copy)

標記-複製算法是在標記-清除算法基礎上進行了優化。

1969年Fenichel在標記-清除算法的基礎上提出一種「半區複製」的垃圾收集算法,其算法流程(標記過程已經省略,標記-整理算法一樣省略該過程)以下:

  1. 內存區域一分爲二,一次使用一半區域,另外一半做爲保留區域。
  2. 當一半區域使用完以後,先將已使用區域中可達(存活)對象分配到保留區域。
  3. 再回收已使用徹底的區域中可回收內存區域,而後使用原先的保留區域。

半區複製

這樣的標記-複製算法對於須要回收大量內存的情形十分友好,而且進行內存複製操做以後不須要考慮內存碎片化問題。 可是相應的也有着其缺點。

  1. 在內存複製搬移過程當中產生了較大的內存複製開銷,而且隨着存活對象的增長,效率也會下降。
  2. 內存實際可用的區域變爲了原先區域的二分之一,浪費內存空間

對於新生代的垃圾收集,這種方式卻是一種有效的思路,只不過比例再也不須要是1:1,IBM針對分代收集理論的第一個假說,新生代中98%的對象熬不過第一輪收集。 而在此基礎上,1989年Andrew Appel提出了一種更加優化的算法。

Appel式回收

將新生代分爲三部分:80%的Eden空間、10%的Survivor(稱爲from)空間、10%的Survivor(稱爲to)空間。

可供分配使用的內存爲80%的Eden空間和10%的名爲from的Survivor空間,當可供分配的使用完以後,JVM將進行垃圾回收,其步驟以下:

  1. 將Eden與from空間的可達對象複製到to空間
  2. 清除回收Eden與from空間內全部可回收對象
  3. 原先的from空間變爲了下一次的to空間,原先的to空間就變爲了下一次的from空間。

Appel式回收

若是存活對象超過這10%的Survivor空間又該怎麼辦呢?別急,還有老年代兜着底,拿出一部分用來放置多出的存活對象。

標記-整理算法(Mark-Compact)

標記-複製算法對於新生代的垃圾回收相對來講十分有效,但對於老年代來講,這種算法不太適宜,由於老年代須要考慮極端狀況即內存全被佔用,所以1974年Edwaed LueDers提出了另外有針對性的算法即標記-整理算法。 其算法流程以下:

  1. 將全部標記的可達對象移動到內存區域的一端,造成可達對象邊界區
  2. 清理除邊界以外的全部對象

標記-整理算法

與標記-清除算法相比,標記-整理算法其實只是多了一個移動可達對象的操做,這樣作的好處是內存分配時不用考慮碎片問題,而這樣作的壞處是內存回收會變得更加複雜。

總結

學習完以後,咱們知道,JVM中的垃圾回收主要針對的是Java堆,而本文也主要針對Java堆的垃圾收集進行分析:垃圾回收的對象斷定(引用計數算法 && 可達性分析以及二次斷定)、劃分不一樣區域並針對不一樣區域提出的垃圾收集算法。這一過程仍是比較複雜的,仍是須要你們多多學習呀~

針對垃圾收集算法,總結了以下一張小表格給你們,但願可以幫助你們快速瞭解下三種算法的優缺點。

垃圾收集算法 優勢 缺點
標記-清除算法 算法設計簡單 效率較低,回收後容易內存碎片化
標記-複製算法 針對新生代相對高效,不用考慮內存碎片化問題 浪費部份內存空間,且有內存複製的開銷
標記-整理算法 針對老年代相對高效,不用考慮內存碎片化問題 設計較爲複雜,且有內存移動的開銷

垃圾回收的過程就是JVM回收內存的過程,這對於之後排查內存問題(如內存泄漏、內存溢出等等)有着重大的幫助,但願你們看完以後,能有所收穫。

相關文章
相關標籤/搜索