1. 程式人生 > >深入淺出 JVM GC(1)

深入淺出 JVM GC(1)

image 本地方法棧 將在 lec 需要 block 釋放 nec 底層

# 前言

初級 Java 程序員步入中級程序員的有一個無法繞過的階段------GC(Garbage Collection)。作為 Java 程序員,說實話,很幸福,不用像 C 程序員那樣,時刻關心著內存,就像網上有句名言------生活從來都不容易,只不過是有人替你負重前行!是的,GC 在替我們做這些臟活累活,GC 像讓我們把精力都放在業務上,而不用每時每刻都在想著內存。現在,GC 也是每個語言的標準配置了。不然誰會去使用這個語言呢?

然而,作為一個合格的程序員,對底層的好奇是進步的動力,如果一個程序員失去了好奇心,那就可以說他在程序員這條道路上就結束了。

難道我們不好奇 GC 到底是怎麽做的嗎?接下來,我們就分析 GC 做了哪些事情。

實際上,GC 主要做3件事情:

  1. 哪些內存需要回收?
  2. 什麽時候回收?
  3. 如何回收?

說到底,GC 就是做這3件事情,如果你能解決這3個問題,那麽你也可以實現一個 GC。

那我們就一個一個問題來看看。

1. 哪些內存需要回收

還記得我們之前分享的關於 JVM 運行時數據區嗎?有堆,有棧,有方法區(永久代),還有直接內存,還有 PC 寄存器。其中,GC 的主要戰場就是堆,當然,方法區也是需要 GC 的。但重點還是堆。

我們知道,堆中內存是共享的,基本所有的對象都是在堆中創建。當一個對象不需要使用了,理論上我們就需要釋放他所占用的內存。

問題來了,如何分辨一個對象不需要使用了呢?答案是:不可能被任何途徑使用的對象。也就是說他沒有了任何引用。我們知道,引用在棧中,實例在堆中,當一個實例沒有了指向他的引用,我們認為,這個實例就需要清除並釋放他所占用的內存了。

那麽 GC 是如何實現的呢?一般而言有2種方法:

  1. 引用計數法(有缺陷,無法解決循環引用問題,JVM 沒有采用)
  2. 可達性分析(解決了引用計數的缺陷,被 JVM 采用)

什麽是引用計數法呢?

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

雖然乍看這個算法簡單,效率也高,但有一個問題這個算法無法解決,就是循環引用。試想一下:A 對象引用了 B,B 對象也引用了 A,但 A 和 B 都不被別的地方使用,也就是說,實際上這兩個對象是垃圾對象,但是由於他們互相持有引用,導致他們的引用計數器都不為0,因此系統無法判斷是垃圾,也無法回收他們。

所以,在現在的 JVM 中,是沒有使用這個算法的。我們知道就行。

引用計數法不行,那就再說說可達性分析算法。

這個算法的基本思想就是通過一系列的稱為 “GC Roots” 的對象作為起始點,從這些節點開始向下搜索,所有所走過的路徑稱為引用鏈(Reference Chain),當一個對象到 GC Roots 沒有任何引用鏈相連(也就是對象不可達)時,則證明此對象是不可用的。如下圖所示,obj5 , obj6, obj7 雖然互相有關聯,但是他們到 GC Roots 是不可達的,所以他們將會判定為是可回收的對象。

技術分享圖片

那麽哪些對象可以作為 GC Roots 對象呢?

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  2. 方法區中類靜態屬性引用的對象。
  3. 方法區中常量引用的對象。
  4. 本地方法棧中 JNI (即 native 方法)引用的對象。

2. 什麽時候回收?

註意:即使是在可達性分析算法中不可達的對象,也並非是"非死不可的",這時候他們實際上是處於 “緩刑” 階段。因為要真正宣告一個對象的死亡,至少需要經歷兩次標記過程:

> 如果對象在進行可達性分析後發現沒有與 GC Roots 相連接的引用鏈,那他將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize 方法。註意:當對象沒有覆蓋 finalize 方法,或者 finalize 方法已經被虛擬機調用過,虛擬機將這兩種情況都視為 “沒有必要執行”。也就是說,finalize 方法只會被執行一次。

如果這個對象被判定為有必要執行 finalize 方法,那麽這個對象將會放置在一個叫做 F-Queue 的隊列之中,並在稍後由一個虛擬機自動建立的,低優先級的 Finalizer 線程去執行它。註意:如果一個對象在 finalize 方法中運行緩慢,將會導致隊列後的其他對象永遠等待,嚴重時將會導致系統崩潰。

finalize 方法是對象逃脫死亡命運的最後一道關卡。稍後 GC 將對隊列中的對象進行第二次規模的標記,如果對象要在 finalize 中 “拯救” 自己,只需要將自己關聯到引用上即可,通常是 this。如果這個對象關聯上了引用,那麽在第二次標記的時候他將被移除出 “即將回收” 的集合;如果對象這時候還沒有逃脫,那基本上就是真的被回收了。

這裏需要註意的一點就是:一個對象如果重寫了 finalize 方法,那麽這個方法最多只會被執行一次。

建議:如非必要,不要重寫該方法。可以使用 try-finally 代替,此方式更好,更及時。同時註意:在 Mysql 的 JDBC 驅動中,com.mysql.jdbc.ConnectionImpl 就實現了 finalize 方法,作用是:當一個 JDBC Connection 被回收時,需要進行連接的關閉,如果開發人員忘記了關閉,則在 finalize 方法中進行關閉。但是,由於其調用的不確定性,這不能單獨作為可靠的資源回收手段。

到這裏,我們知道了什麽時候進行回收:如果一個對象重寫了 finalize 方法且這個方法沒有被 JVM 調用過,那麽這個對象會被放入一個隊列等待被一個低優先級的線程執行 finalize 方法,如果在這個方法中對象不能自救,則這個對象在第二次標記過程中就會被標記死亡,等待 GC 回收。

3. 如何回收?

如何回收,這個問題非常的大,涉及到各種垃圾回收算法,各種垃圾收集器。限於本篇的篇幅,樓主將不會在這篇文章裏深入探討,這裏只會列出一些大綱,這些大綱將是後面文章的摘要,我們將在後面的文章中深入探討如何回收。

那麽,有哪些摘要呢?

3.1 垃圾回收算法
  1. 標記清除算法
  2. 復制算法
  3. 標記整理算法
  4. 分代收集算法(堆如何分代)

這些算法是 GC 的基礎,所有 GC 的實現都是基於這些算法來清除無用對象,然後釋放內存空間。我們將會在後面的文章一個一個講解。

3.2 有哪些垃圾收集器
  1. Serial 串行收集器(只適用於堆內存256m 一下的 JVM )
  2. ParNew 並行收集器(Serial 收集器的多線程版本)
  3. Parallel Scavenge (PS 收集器,該收集器以吞吐量為主要目的,是1.8的默認 GC)
  4. CMS 收集器(該收集器全稱 Concurrent Mark Sweep,是一種關註最短停頓時間的垃圾收集器)
  5. G1 收集器(JDK 9 的默認 GC)
3.3 有哪些GC
  1. Young GC(又稱 YGC,minor GC,年輕代 GC)
  2. Old GC (老年代 GC,只有 CMS 才會單獨回收 Old 區)
  3. Full GC(又稱 major GC)
  4. Mixed GC(混合 GC,G1 收集器獨有)

好,以上就是如何回收的大綱,我們將在後面的文章中慢慢講解。

總結

這篇文章主要總結了什麽是 GC ,以及 GC 的作用,GC 主要做了3件事情,哪些內存需要回收,什麽時候回收,如何回收。我們知道了 GC 通過可達性分析知道了哪些內存需要回收,那什麽時候回收呢?執行 finalize 方法後如果還沒有復活,將被回收。第三個問題:如何回收呢?這個問題是一個大課題,我們只是列出了一些大綱,比如有哪些垃圾收集器,有哪些垃圾算法,有哪些 GC 過程。這些細節我們將在後面慢慢講解,逐步深入。

深入淺出 JVM GC(1)