深入淺出 JVM GC(1)
# 前言
初級 Java 程序員步入中級程序員的有一個無法繞過的階段------GC(Garbage Collection)。作為 Java 程序員,說實話,很幸福,不用像 C 程序員那樣,時刻關心著內存,就像網上有句名言------生活從來都不容易,只不過是有人替你負重前行!是的,GC 在替我們做這些臟活累活,GC 像讓我們把精力都放在業務上,而不用每時每刻都在想著內存。現在,GC 也是每個語言的標準配置了。不然誰會去使用這個語言呢?
然而,作為一個合格的程序員,對底層的好奇是進步的動力,如果一個程序員失去了好奇心,那就可以說他在程序員這條道路上就結束了。
難道我們不好奇 GC 到底是怎麽做的嗎?接下來,我們就分析 GC 做了哪些事情。
實際上,GC 主要做3件事情:
- 哪些內存需要回收?
- 什麽時候回收?
- 如何回收?
說到底,GC 就是做這3件事情,如果你能解決這3個問題,那麽你也可以實現一個 GC。
那我們就一個一個問題來看看。
1. 哪些內存需要回收
還記得我們之前分享的關於 JVM 運行時數據區嗎?有堆,有棧,有方法區(永久代),還有直接內存,還有 PC 寄存器。其中,GC 的主要戰場就是堆,當然,方法區也是需要 GC 的。但重點還是堆。
我們知道,堆中內存是共享的,基本所有的對象都是在堆中創建。當一個對象不需要使用了,理論上我們就需要釋放他所占用的內存。
問題來了,如何分辨一個對象不需要使用了呢?答案是:不可能被任何途徑使用的對象。也就是說他沒有了任何引用。我們知道,引用在棧中,實例在堆中,當一個實例沒有了指向他的引用,我們認為,這個實例就需要清除並釋放他所占用的內存了。
那麽 GC 是如何實現的呢?一般而言有2種方法:
- 引用計數法(有缺陷,無法解決循環引用問題,JVM 沒有采用)
- 可達性分析(解決了引用計數的缺陷,被 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 對象呢?
- 虛擬機棧(棧幀中的本地變量表)中引用的對象。
- 方法區中類靜態屬性引用的對象。
- 方法區中常量引用的對象。
- 本地方法棧中 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 垃圾回收算法
- 標記清除算法
- 復制算法
- 標記整理算法
- 分代收集算法(堆如何分代)
這些算法是 GC 的基礎,所有 GC 的實現都是基於這些算法來清除無用對象,然後釋放內存空間。我們將會在後面的文章一個一個講解。
3.2 有哪些垃圾收集器
- Serial 串行收集器(只適用於堆內存256m 一下的 JVM )
- ParNew 並行收集器(Serial 收集器的多線程版本)
- Parallel Scavenge (PS 收集器,該收集器以吞吐量為主要目的,是1.8的默認 GC)
- CMS 收集器(該收集器全稱 Concurrent Mark Sweep,是一種關註最短停頓時間的垃圾收集器)
- G1 收集器(JDK 9 的默認 GC)
3.3 有哪些GC
- Young GC(又稱 YGC,minor GC,年輕代 GC)
- Old GC (老年代 GC,只有 CMS 才會單獨回收 Old 區)
- Full GC(又稱 major GC)
- Mixed GC(混合 GC,G1 收集器獨有)
好,以上就是如何回收的大綱,我們將在後面的文章中慢慢講解。
總結
這篇文章主要總結了什麽是 GC ,以及 GC 的作用,GC 主要做了3件事情,哪些內存需要回收,什麽時候回收,如何回收。我們知道了 GC 通過可達性分析知道了哪些內存需要回收,那什麽時候回收呢?執行 finalize 方法後如果還沒有復活,將被回收。第三個問題:如何回收呢?這個問題是一個大課題,我們只是列出了一些大綱,比如有哪些垃圾收集器,有哪些垃圾算法,有哪些 GC 過程。這些細節我們將在後面慢慢講解,逐步深入。
深入淺出 JVM GC(1)