弄清Java虛擬機器GC的執行過程

coderbruis發表於2018-12-05

前言:要弄清Java虛擬機器GC的整個過程,就得弄明白Java虛擬機器用什麼來進行GC?Java虛擬機器在哪裡GC?什麼時候GC?GC什麼?

開門見山

GC(Garbage Collection)垃圾收集,JVM一個非常重要的功能。本文將圍繞著JVM的GC這個動作展開,來過一遍GC的整個運作過程。

JVM用什麼來進行GC

JVM是GC的發起者,準確說是VMThread是GC的發起者,那用什麼來進行GC呢?很顯然,用到的是垃圾收集器來進行GC的。而垃圾收集器,可以根據堆中的分代,分為不同型別的垃圾收集器。

新生代(Young generation)

  • Serial垃圾收集器
  • ParNew垃圾收集器
  • Parallel Scavenge垃圾收集器

老年代(Tenured generation)

  • CMS垃圾收集器
  • Parallel Old垃圾收集器
  • Serial Old垃圾收集器

G1是一個特殊的垃圾收集器,既可以作為新生代的垃圾收集器,也可以作為老年代的垃圾收集器。

image

更加詳細的垃圾收集器的知識,可以閱讀《深入理解JVM》這本書。

JVM在哪裡進行GC

JVM的GC動作只在兩個地方回收————堆和方法區。(JDK8的metaspace的GC,這裡暫不討論。如果還有其他記憶體區域發生垃圾回收,請指正)

在堆取進行GC

首先,得了解堆是什麼,有什麼作用?言簡意賅——總結Java記憶體區域和常量池

這裡需要知道,JVM的GC採用了分代思想,所以堆被分成了新生代和老年代,而新生代又被細分為Eden區和From survivor和To survivor區。當類被JVM載入後,Java應用程式執行然後某個物件被new,這個物件就會被分配到堆中的新生代的Eden區(物件優先被分配到Eden區),如果Eden區域沒有足夠的記憶體來分配給該物件,就會觸發minor GC來清除已經“死亡”的物件,這樣才能將新清出的記憶體分配給該物件,經過GC都還存活的物件,會被移至From survivor區中。由於新生代採用的複製演算法,Eden區存活物件和From survivor區的存活物件將被複制到To survivor區中。在To survivor區中的物件,每經過一次GC,物件中的“年齡計數器”就會加1,如果超過了晉升為老年代的年齡閾值時(預設為15)物件就會晉升到老年代中。

由於老年代裡存放的都是大物件、存活時間較久的物件,因此老年代一般都是用標記-整理演算法或標記-清除演算法。當老年代物件沒法再分配記憶體時,會觸發一次Major GC(Full GC),用於回收那些已經“死亡”的物件。

下圖為堆中分代

image

在方法區中進行GC

在堆中進行GC一般可以回收70%~95%的空間,相比在方法區中進行GC效率是非常低的。但是效率低不代表不進行GC。永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。

JVM什麼時候進行GC

在上文中,已經講到了JVM什麼時候進行GC。就是在Eden區沒有足夠記憶體分配給物件的時候進行Minor GC,在老年代沒法再分配記憶體給大物件以及“老”物件的時候進行的Major GC(full GC)。這裡談一談Minor GC和Major GC:

  • 新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因為Java物件大多都具備招生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也非常快。
  • 老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC(並不是絕對的,例如Parallel Scavenge垃圾收集器就直接進行Major GC的策略選擇過程)。Major GC的速度一般比Minor GC慢10倍以上。

JVM對什麼物件進行GC

這裡,首先得了解物件在什麼情況下會被進行GC?

是“真正死亡”的物件嗎?

那麼,物件怎麼才能被判定為“真正死亡”呢?JVM是通過可達性分析來進行分析的。可達性分析就是通過從GC Roots為起點,判斷是否有一個引用鏈與被判斷的物件相連,如果相連這代表這個物件還“活著”,是可用的。然而,一個物件被真正判定為“死亡”,需要進行兩次標記(被標記為可回收物件),然後在下一次JVM進行GC的時候,才被真正的回收掉。如果一個物件沒有與GC Roots的引用鏈相連線,也並沒有被真正判定為“死亡”,而是進行第一次標記,然後在第二次標記之前會進行“自救”過程。所謂的“自救”就是在第二次被標記之前,需要重新與引用鏈相連線。在第一次被標記後,物件會進行篩選,篩選的條件為是否有必要進行執行finalize()方法。如果沒有必要執行finalize()方法,則就等待第二次被標記;如果有必要執行finalize()方法,則會先將物件放置在一個叫F-Queue的佇列中,然後等待虛擬機器通過Finalizer執行緒去觸發物件的finalize()方法,然後在finalize()方法裡面,物件就開始自救的過程。物件自救可以通過把this賦值給某個類變數或者物件的成員變數,就實現了和引用鏈重新連線的目的——自救成功。因此在第二次標記的時候就會把物件從F-Queue佇列中移除,然後虛擬機器會在F-Queue中進行第二次小規模的標記,然後就等待下一次GC的來臨。

結論:

所以,從上面分析的結果來看,JVM進行GC的物件,就是沒有和引用鏈上相連的並且經過第一次標記,沒有“自救”成功的物件。

相關文章