Java GC:基礎原理

Java 使用了垃圾收集器來代替手動管理內存,對於垃圾收集器來講,不管哪一種,其核心思想都是作兩件事:算法

  1. 找到哪些對象是存活的(還在使用)
  2. 清除死掉的(再也不使用)的對象

標記存活對象:

引用計數法

最直接,最容易想到的標記方法是引用計數法,顧明思議,記錄每一個對象被引用的個數,若是爲0,則爲死亡對象。該方法實現簡單,判斷效率高,但很難解決對象之間相互循環引用的問題。 post

可達性分析計算

在JVM中使用了可達性分析計算的方式來標記存活對象,GC 定義了一些特殊的對象做爲 GC Roots:優化

  • 棧幀中的本地變量和參數
  • 活躍線程
  • 已加載的靜態變量
  • JNI 引用

以 GC Roots 做爲起始點,沿着引用路徑不斷搜索,同時標記搜索到的對象爲存活。 線程

注意,在標記階段,須要中止應用線程(Stop the World),由於沒有辦法在應用程序不斷改變引用關係的同時一邊標記。暫停的時間取決於存活對象的多少,存活的對象越多,須要標記的時間越長。3d

清除死亡對象

Sweep and Compact

在學術上,標記清除算法是最具表明性的算法:cdn

Marking: 經過 GC Roots 開始搜索標記可達的對象對象

Sweeping: 使得被未標記的對象佔用的內存空間能夠被以後分配使用blog

可是這樣直接 Sweep 會存在兩個問題:圖片

  • 寫操做時須要尋找可用塊,會更加費時
  • 空間碎片太多會致使分配較大對象時沒法獲得足夠連續內存

爲了不這個問題,還須要作一次碎片整理 內存

Copy

還有一種更簡單的方法,將內存分爲兩塊,每次只使用其中的一塊區域,發生 GC 時,將存活的對象複製到另外一個區域中,這樣也不會出現碎片的問題,此外,複製能夠在標記的同時進行,更加高效。缺點也很明顯,須要更多的內存。這種算法被稱爲標記-複製算法。

分代假說

研究人員觀察到,應用程序內的大多數分配分爲兩類:

  • 大部分對象建立後很快就再也不使用
  • 有一些對象會存活很長一段時間

基於這個假設,虛擬機將內存分爲了兩個代,分別爲 新生代(Young Generation), 和老年代(Old Generation or Tenured)。

那麼針對不一樣代的特色,能夠有針對性的進行算法優化,通常來講將算法分爲 Mionr GC(只回收新生代對象)和 Full GC(全局回收)。 這個假設也存在兩個問題:

  • 兩個代之間的對象可能存在引用,即便只進行 Mionr gc,也須要掃描一遍老年代對象檢查是否存在老年代對象引用新生代對象,違背了分代的初衷
  • 分代假設可能不適用於某些應用。因爲GC對算法是專門針對快速死亡的對象和存活長時間的對象進行了優化,所以對有「中等」壽命的對象的處理,JVM 表現的不太好

內存劃分

一般狀況下 Eden 是對象建立時被分配的區域。因爲涉及到多個線程同時建立對象,Eden 被劃分紅了一個或多個 Thread Local Allocation Buffer (TLAB) ,簡單來講,每一個線程都被分配了一塊區域用於本線程的對象分配(避免的線程同步代價),若是分配的內存不夠使用了,則使用共有的部分(申請新的TLAB),若是再不夠,則觸發一次新生代 GC(Minor GC) ,若是清理後的內存仍然不夠,則將對象分配在老年代。

在 Mionr GC 時,首先經過 GC Roots 掃描標記全部存活的對象,須要注意以前提到過,老年代的對象也有可能引用新生代對象。對於這個問題,JVM 使用了 card-marking 來避免老年代的掃描。HotSpot 使用了卡表(Card Table)的技術,將整個堆劃分爲一個個大小爲512字節的卡,若是卡中的對象可能指向新生代對象引用,那麼這張卡是髒的,同時 JVM 維護了一個卡表,每張卡都有一個對應的標識位來表示是不是髒卡。那麼在進行 Minor GC 時,只需將髒卡中的對象將入到 GC Roots 裏,而不用掃描整個老年代。

完成標記後,將全部存活的對象複製到其中一個 Survivor 區中,此後整個 Eden 區的內存均可以從新被使用了。這個算法也叫作「標記-複製「算法(Mark and Copy)。 Survivor 分爲 from 和 to 兩個區域(每次GC後身份互換),其中 to 區域永遠是空的,當GC完成標記後,Eden 和 from 區域的存活對象都複製到 to 區域中,from區域清空,兩個區域身份互換。

對象可能在兩個 Survivor 中不斷的來回複製,當複製達到必定次數時(默認15次),將被認爲足夠老,晉升到老年代中。此外,若是 Survivor 區域大小不夠存放全部存活對象,則會將較老的對象提早晉升到老年代中。

老年代內存要大的多,而且大多數對象都不會是垃圾,而且發生GC的頻率要相對小的多,因此複製算法不適用。通常來講,使用「標記-清除-整理「的算法對老年代進行回收。

至於 JVM 是如如何使用這些算法實現具體的回收器的,請看這篇 通俗易懂 JVM 中的 GC 實現

圖片來源及參考資料:《Plumbr Handbook Java Garbage Collection》

相關文章
相關標籤/搜索