天天看點

垃圾回收機制詳細分析

http://www.importnew.com/26821.html

垃圾回收機制是 Java 非常重要的特性之一,也是面試題的常客。它讓開發者無需關注空間的建立和釋放,而是以守護程序的形式在背景自動回收垃圾。這樣做不僅提高了開發效率,更改善了記憶體的使用狀況。

今天本文來對垃圾回收機制進行講解,主要涉及下面幾個問題:

  • 什麼是堆記憶體?
  • 什麼是垃圾?
  • 有哪些方法回收這些垃圾?
  • 什麼是分代回收機制?

什麼是 Java 堆記憶體

堆是在 JVM 啟動時建立的,主要用來維護運作時資料,如運作過程中建立的對象和數組都是基于這塊記憶體空間。Java 堆是非常重要的元素,如果我們動态建立的對象沒有得到及時回收,持續堆積,最後會導緻堆空間被占滿,記憶體溢出。

是以,Java 提供了一種垃圾回收機制,在背景建立一個守護程序。該程序會在記憶體緊張的時候自動跳出來,把堆空間的垃圾全部進行回收,進而保證程式的正常運作。

那什麼是垃圾呢?

所謂“垃圾”,就是指所有不再存活的對象。常見的判斷是否存活有兩種方法:引用計數法和可達性分析。

引用計數法

為每一個建立的對象配置設定一個引用計數器,用來存儲該對象被引用的個數。當該個數為零,意味着沒有人再使用這個對象,可以認為“對象死亡”。但是,這種方案存在嚴重的問題,就是無法檢測“循環引用”:當兩個對象互相引用,即時它倆都不被外界任何東西引用,它倆的計數都不為零,是以永遠不會被回收。而實際上對于開發者而言,這兩個對象已經完全沒有用處了。

是以,Java 裡沒有采用這樣的方案來判定對象的“存活性”。

可達性分析

這種方案是目前主流語言裡采用的對象存活性判斷方案。基本思路是把所有引用的對象想象成一棵樹,從樹的根結點 GC Roots 出發,持續周遊找出所有連接配接的樹枝對象,這些對象則被稱為“可達”對象,或稱“存活”對象。其餘的對象則被視為“死亡”的“不可達”對象,或稱“垃圾”。

參考下圖,object5,object6 和 object7 便是不可達對象,視為“死亡狀态”,應該被垃圾回收器回收。

垃圾回收機制詳細分析

GC Roots 究竟指誰呢?

我們可以猜測,GC Roots 本身一定是可達的,這樣從它們出發周遊到的對象才能保證一定可達。那麼,Java 裡有哪些對象是一定可達呢?主要有以下四種:

  • 虛拟機棧(幀棧中的本地變量表)中引用的對象。
  • 方法區中靜态屬性引用的對象。
  • 方法區中常量引用的對象。
  • 本地方法棧中 JNI 引用的對象。

不少讀者可能對這些 GC Roots 似懂非懂,這涉及到 JVM 本身的記憶體結構等等,未來的文章會再做深入講解。這裡隻要知道有這麼幾種類型的 GC Roots,每次垃圾回收器會從這些根結點開始周遊尋找所有可達節點。

有哪些方式來回收這些垃圾呢?

上面已經知道,所有 GC Roots 不可達的對象都稱為垃圾,參考下圖,黑色的表示垃圾,灰色表示存活對象,綠色表示空白空間。

垃圾回收機制詳細分析

那麼,我們如何來回收這些垃圾呢?

标記-清理

第一步,所謂“标記”就是利用可達性周遊堆記憶體,把“存活”對象和“垃圾”對象進行标記,得到的結果如上圖;

第二步,既然“垃圾”已經标記好了,那我們再周遊一遍,把所有“垃圾”對象所占的空間直接 清空 即可。

結果如下:

垃圾回收機制詳細分析

這便是 标記-清理 方案,簡單友善 ,但是容易産生 記憶體碎片。

标記-整理

既然上面的方法會産生記憶體碎片,那好,我在清理的時候,把所有 存活 對象紮堆到同一個地方,讓它們待在一起,這樣就沒有記憶體碎片了。

結果如下:

垃圾回收機制詳細分析

這兩種方案适合 存活對象多,垃圾少 的情況,它隻需要清理掉少量的垃圾,然後挪動下存活對象就可以了。

複制

這種方法比較粗暴,直接把堆記憶體分成兩部分,一段時間内隻允許在其中一塊記憶體上進行配置設定,當這塊記憶體被配置設定完後,則執行垃圾回收,把所有 存活 對象全部複制到另一塊記憶體上,目前記憶體則直接全部清空。

參考下圖:

垃圾回收機制詳細分析

起初時隻使用上面部分的記憶體,直到記憶體使用完畢,才進行垃圾回收,把所有存活對象搬到下半部分,并把上半部分進行清空。

這種做法不容易産生碎片,也簡單粗暴;但是,它意味着你在一段時間内隻能使用一部分的記憶體,超過這部分記憶體的話就意味着堆記憶體裡頻繁的 複制清空。

這種方案适合 存活對象少,垃圾多 的情況,這樣在複制時就不需要複制多少對象過去,多數垃圾直接被清空處理。

Java 的分代回收機制

上面我們看到有至少三種方法來回收記憶體,那麼 Java 裡是如何選擇利用這三種回收算法呢?是隻用一種還是三種都用呢?

Java 的堆結構

在選擇回收算法前,我們先來看一下 Java 堆的結構。

一塊 Java 堆空間一般分成三部分,這三部分用來存儲三類資料:

  • 剛剛建立的對象。在代碼運作時會持續不斷地創造新的對象,這些新建立的對象會被統一放在一起。因為有很多局部變量等在新建立後很快會變成 不可達 的對象,快速死去 ,是以這塊區域的特點是 存活對象少,垃圾多 。形象點描述這塊區域為: 新生代;
  • 存活了一段時間的對象。這些對象早早就被建立了,而且一直活了下來。我們把這些 存活時間較長 的對象放在一起,它們的特點是 存活對象多,垃圾少 。形象點描述這塊區域為: 老年代;
  • 永久存在的對象。比如一些靜态檔案,這些對象的特點是不需要垃圾回收,永遠存活。形象點描述這塊區域為:永久代 。(不過在 Java 8 裡已經把 永久代 删除了,把這塊記憶體空間給了 元空間,後續文章再講解。)

也就是說,正常的 Java 堆至少包括了 新生代 和 老年代 兩塊記憶體區域,而且這兩塊區域有很明顯的特征:

  • 新生代:存活對象少、垃圾多
  • 老年代:存活對象多、垃圾少

結合新生代/老年代的存活對象特點和之前提過的幾種垃圾回收算法,可以得到如下的回收方案:

新生代-複制 回收機制

對于新生代區域,由于每次 GC 都會有大量新對象死去,隻有少量存活。是以采用 複制 回收算法,GC 時把少量的存活對象複制過去即可。

那麼如何設計這個 複制 算法比較好呢?有以下幾種方式:

思路 1. 把記憶體均分成 1:1 兩等份

如下圖拆分記憶體。

垃圾回收機制詳細分析
垃圾回收機制詳細分析

每次隻使用一半的記憶體,當這一半滿了後,就進行垃圾回收,把存活的對象直接複制到另一半記憶體,并清空目前一半的記憶體。

這種分法的缺陷是相當于隻有一半的可用記憶體,對于新生代而言,新對象持續不斷地被建立,如果隻有一半可用記憶體,那顯然要持續不斷地進行垃圾回收工作,反而影響到了正常程式的運作,得不償失。

思路 2. 把記憶體按 9:1 分

既然上面的分法導緻可用記憶體隻剩一半,那麼我做些調整,把 1:1變成9:1,

最開始在 9 的記憶體區使用,當 9 快要滿時,執行複制回收,把 9 内仍然存活的對象複制到 1 區,并清空 9 區。

這樣看起來是比上面的方法好了,但是它存在比較嚴重的問題。

當我們把 9 區存活對象複制到 1 區時,由于記憶體空間比例相差比較大,是以很有可能 1 區放不滿,此時就不得不把對象移到 老年區 。而這就意味着,可能會有一部分 并不老 的 9 區對象由于 1 區放不下了而被放到了 老年區 ,可想而知,這破壞了 老年區 的規則。或者說,一定程度上的 老年區 并不一定全是 老年對象。

那應該如何才能把真正比較 老 的對象挪到 老年區 呢?

思路 3. 把記憶體按 8:1:1 分

既然 9:1 有可能把年輕對象放到 老年區 ,那就換成 8:1:1,依次取名為 Eden、Survivor A、Survivor B 區,其中 Eden 意為伊甸園,形容有很多新生對象在裡面建立;Survivor區則為幸存者,即經曆 GC 後仍然存活下來的對象。

工作原理如下:

  1. 首先,Eden區最大,對外提供堆記憶體。當 Eden 區快要滿了,則進行 Minor GC,把存活對象放入 Survivor A 區,清空 Eden 區;
  2. Eden區被清空後,繼續對外提供堆記憶體;
  3. 當 Eden 區再次被填滿,此時對 Eden 區和 Survivor A 區同時進行 Minor GC,把存活對象放入 Survivor B 區,同時清空 Eden 區和Survivor A 區;
  4. Eden區繼續對外提供堆記憶體,并重複上述過程,即在 Eden 區填滿後,把 Eden 區和某個 Survivor 區的存活對象放到另一個 Survivor 區;
  5. 當某個 Survivor 區被填滿,且仍有對象未被複制完畢時,或者某些對象在反複 Survive 15 次左右時,則把這部分剩餘對象放到Old 區;
  6. 當 Old 區也被填滿時,進行 Major GC,對 Old 區進行垃圾回收。

[注意,在真實的 JVM 環境裡,可以通過參數 SurvivorRatio 手動配置 Eden 區和單個 Survivor 區的比例,預設為 8。]

那麼,所謂的 Old 區垃圾回收,或稱Major GC,應該如何執行呢?

老年代-标記整理 回收機制

根據上面我們知道,老年代一般存放的是存活時間較久的對象,是以每一次 GC 時,存活對象比較較大,也就是說每次隻有少部分對象被回收。

是以,根據不同回收機制的特點,這裡選擇 存活對象多,垃圾少 的标記整理 回收機制,僅僅通過少量地移動對象就能清理垃圾,而且不存在記憶體碎片化。

至此,我們已經了解了 Java 堆記憶體的分代原理,并了解了不同代根據各自特點采用了不同的回收機制,即 新生代 采用 回收 機制,老年代 采用 标記整理 機制。

小結

垃圾回收是 Java 非常重要的特性,也是進階 Java 工程師的必經之路。