在前面幾個課時中,我們不止一次提到了堆(heap),堆是一個巨大的對象池。在這個對象池中管理著數量巨大的對象實例。
而池中對象的引用層次,有的是很深的。一個被頻繁調用的接口,每秒生成對象的速度,也是非常可觀的。對象之間的關系,形成了一張巨大的網。雖然 Java 一直在營造一種無限內存的氛圍,但對象不能只增不減,所以需要垃圾回收。
那 JVM 是如何判斷哪些對象應該被回收?哪些應該被保持呢?
在古代,刑罰中有誅九族一說。指的是有些人犯大事時,皇上殺一人不足以平復內心的憤怒時,會對親朋好友產生連帶責任。誅九族時首先需要追溯到一個共同的祖先,再往下細數連坐。堆上的垃圾回收也有同樣的思路。我們接下來就具體分析 JVM 中是如何進行垃圾回收的。
JVM 的 GC 動作,是不受程序控制的,它會在滿足條件的時候,自動觸發。
在發生 GC 的時候,一個對象,JVM 總能夠找到引用它的祖先。找到最后,如果發現這個祖先已經名存實亡了,它們都會被清理掉。而能夠躲過垃圾回收的那些祖先,比較特殊,它們的名字就叫作 GC Roots。
從 GC Roots 向下追溯、搜索,會產生一個叫作 Reference Chain 的鏈條。當一個對象不能和任何一個 GC Root 產生關系時,就會被無情的誅殺掉。
如圖所示,Obj5、Obj6、Obj7,由于不能和 GC Root 產生關聯,發生 GC 時,就會被摧毀。

垃圾回收就是圍繞著 GC Roots 去做的。同時,它也是很多內存泄露的根源,因為其他引用根本沒有這樣的權利。
那么,什么樣的對象,才會是 GC Root 呢?這不在于它是什么樣的對象,而在于它所處的位置。
### GC Roots 有哪些
GC Roots 是一組必須活躍的引用。用通俗的話來說,就是程序接下來通過直接引用或者間接引用,能夠訪問到的潛在被使用的對象。
GC Roots 包括:
* Java 線程中,當前所有正在被調用的方法的引用類型參數、局部變量、臨時值等。也就是與我們棧幀相關的各種引用。
* 所有當前被加載的 Java 類。
* Java 類的引用類型靜態變量。
* 運行時常量池里的引用類型常量(String 或 Class 類型)。
* JVM 內部數據結構的一些引用,比如 sun.jvm.hotspot.memory.Universe 類。
* 用于同步的監控對象,比如調用了對象的 wait() 方法。
* JNI handles,包括 global handles 和 local handles。
這些 GC Roots 大體可以分為三大類,下面這種說法更加好記一些:
* 活動線程相關的各種引用。
* 類的靜態變量的引用。
* JNI 引用。

有兩個注意點:
* 我們這里說的是活躍的引用,而不是對象,對象是不能作為 GC Roots 的。
* GC 過程是找出所有活對象,并把其余空間認定為“無用”;而不是找出所有死掉的對象,并回收它們占用的空間。所以,哪怕 JVM 的堆非常的大,基于 tracing 的 GC 方式,回收速度也會非常快。
#### 引用級別
接下來的一道面試題就有意思多了:能夠找到 Reference Chain 的對象,就一定會存活么?
我在面試的時候,經常會問這些問題,比如“弱引用有什么用處”?令我感到奇怪的是,即使是一些工作多年的 Java 工程師,對待這個問題也是一知半解,錯失了很多機會。
對象對于另外一個對象的引用,要看關系牢靠不牢靠,可能在鏈條的其中一環,就斷掉了。

根據發生 GC 時,這條鏈條的表現,可以對這個引用關系進行更加細致的劃分。
它們的關系,可以分為強引用、軟引用、弱引用、虛引用等。
#### 強引用 Strong references
當內存空間不足,系統撐不住了,JVM 就會拋出 OutOfMemoryError 錯誤。即使程序會異常終止,這種對象也不會被回收。這種引用屬于最普通最強硬的一種存在,只有在和 GC Roots 斷絕關系時,才會被消滅掉。
這種引用,你每天的編碼都在用。例如:new 一個普通的對象。
```
Object obj = new Object()
```
這種方式可能是有問題的。假如你的系統被大量用戶(User)訪問,你需要記錄這個 User 訪問的時間。可惜的是,User 對象里并沒有這個字段,所以我們決定將這些信息額外開辟一個空間進行存放。
```
static Map<User,Long> userVisitMap = new HashMap<>();
...
userVisitMap.put(user, time);
```
當你用完了 User 對象,其實你是期望它被回收掉的。但是,由于它被 userVisitMap 引用,我們沒有其他手段 remove 掉它。這個時候,就發生了內存泄漏(memory leak)。
這種情況還通常發生在一個沒有設定上限的 Cache 系統,由于設置了不正確的引用方式,加上不正確的容量,很容易造成 OOM。
#### 軟引用 Soft references
軟引用用于維護一些可有可無的對象。在內存足夠的時候,軟引用對象不會被回收,只有在內存不足時,系統則會回收軟引用對象,如果回收了軟引用對象之后仍然沒有足夠的內存,才會拋出內存溢出異常。
可以看到,這種特性非常適合用在緩存技術上。比如網頁緩存、圖片緩存等。
Guava 的 CacheBuilder,就提供了軟引用和弱引用的設置方式。在這種場景中,軟引用比強引用安全的多。
軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收,Java 虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。
我們可以看一下它的代碼。軟引用需要顯式的聲明,使用泛型來實現。
```
// 偽代碼
Object object = new Object();
SoftReference<Object> softRef = new SoftReference(object);
```
這里有一個相關的 JVM 參數。它的意思是:每 MB 堆空閑空間中 SoftReference 的存活時間。這個值的默認時間是1秒(1000)。
```
-XX:SoftRefLRUPolicyMSPerMB=<N>
```
這里要特別說明的是,網絡上一些流傳的優化方法,即把這個值設置成 0,其實是錯誤的,這樣容易引發故障,感興趣的話你可以自行搜索一下。
這種比較偏門的優化手段,除非在你對其原理相當了解的情況下,才能設置一些比較特殊的值。比如 0 值,無限大等,這種值在 JVM 的設置中,最好不要發生。
#### 弱引用 Weak references
弱引用對象相比較軟引用,要更加無用一些,它擁有更短的生命周期。
當 JVM 進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。弱引用擁有更短的生命周期,在 Java 中,用 java.lang.ref.WeakReference 類來表示。
它的應用場景和軟引用類似,可以在一些對內存更加敏感的系統里采用。它的使用方式類似于這段的代碼:
```
// 偽代碼
Object object = new Object();
WeakReference<Object> softRef = new WeakReference(object);
```
#### 虛引用 Phantom References
這是一種形同虛設的引用,在現實場景中用的不是很多。虛引用必須和引用隊列(ReferenceQueue)聯合使用。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。
實際上,虛引用的 get,總是返回 null。
```
Object object = new Object();
ReferenceQueue queue = new ReferenceQueue();
// 虛引用,必須與一個引用隊列關聯
PhantomReference pr = new PhantomReference(object, queue);
```
虛引用主要用來跟蹤對象被垃圾回收的活動。
當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象之前,把這個虛引用加入到與之關聯的引用隊列中。
程序如果發現某個虛引用已經被加入到引用隊列,那么就可以在所引用的對象的內存被回收之前采取必要的行動。
下面的方法,就是一個用于監控 GC 發生的例子。
```
private static void startMonitoring(ReferenceQueue<MyObject> referenceQueue, Reference<MyObject> ref) {
ExecutorService ex = Executors.newSingleThreadExecutor();
ex.execute(() -> {
while (referenceQueue.poll()!=ref) {
//don't hang forever
if(finishFlag){
break;
}
}
System.out.println("-- ref gc'ed --");
});
ex.shutdown();
}
```
基于虛引用,有一個更加優雅的實現方式,那就是 Java 9 以后新加入的 Cleaner,用來替代 Object 類的 finalizer 方法。
### 典型 OOM 場景
OOM 的全稱是 Out Of Memory,那我們的內存區域有哪些會發生 OOM 呢?我們可以從內存區域劃分圖上,看一下彩色部分。

可以看到除了程序計數器,其他區域都有OOM溢出的可能。但是最常見的還是發生在堆上。

所以 OOM 到底是什么引起的呢?有幾個原因:
* 內存的容量太小了,需要擴容,或者需要調整堆的空間。
* 錯誤的引用方式,發生了內存泄漏。沒有及時的切斷與 GC Roots 的關系。比如線程池里的線程,在復用的情況下忘記清理 ThreadLocal 的內容。
* 接口沒有進行范圍校驗,外部傳參超出范圍。比如數據庫查詢時的每頁條數等。
* 對堆外內存無限制的使用。這種情況一旦發生更加嚴重,會造成操作系統內存耗盡。
典型的內存泄漏場景,原因在于對象沒有及時的釋放自己的引用。比如一個局部變量,被外部的靜態集合引用。

你在平常寫代碼時,一定要注意這種情況,千萬不要為了方便把對象到處引用。即使引用了,也要在合適時機進行手動清理。關于這部分的問題根源排查,我們將在實踐課程中詳細介紹。
### 小結
你可以注意到 GC Roots 的專業叫法,就是可達性分析法。另外,還有一種叫作引用計數法的方式,在判斷對象的存活問題上,經常被提及。
因為有循環依賴的硬傷,現在主流的 JVM,沒有一個是采用引用計數法來實現 GC 的,所以我們大體了解一下就可以。引用計數法是在對象頭里維護一個 counter 計數器,被引用一次數量 +1,引用失效記數 -1。計數器為 0 時,就被認為無效。你現在可以忘掉引用計數的方式了。
本課時,我們詳細介紹了 GC Roots 都包含哪些內容。HostSpot 采用 tracing 的方式進行 GC,內存回收的速度與處于 living 狀態的對象數量有關。
這部分涉及的內容較多,如果面試被問到,你可以采用白話版的方式進行介紹,然后舉例深入。
接下來,我們了解到四種不同強度的引用類型,尤其是軟引用和虛引用,在平常工作中使用還是比較多的。這里面最不常用的就是虛引用,但是它引申出來的 Cleaner 類,是用來替代 finalizer 方法的,這是一個比較重要的知識點。
本課時最后討論了幾種典型的 OOM 場景,你可能現在對其概念比較模糊。接下來的課時,我們將詳細介紹幾個常見的垃圾回收算法,然后對這些 OOM 的場景逐個擊破。
### 課后問答
* 1、為什么虛引用在回收之前必須加入到與之關聯的隊列中,而其他引用可以不需要?
答案:其他的如果有需要也可以加入;但虛引用的唯一用途就是這個,所以要加。
* 2、對于軟引用存活時間為1秒不理解,軟引用不是在內存不足時才清除嗎?
答案:SoftReference里面,有一個timestamp,記錄了上次GC的時間。對某個軟引用來說,GC時空間足不足,通過以下規則計算:
```
now - timestamp <= 空閑空間 * SoftRefLRUPolicyMSPerMB
```
屬于內部實現細節,不建議改動。
* 3、"軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收,Java 虛擬機就會把這個軟引用加入到與之關聯的引用隊列中"請問這個隊列有什么作用呢?加入之后干什么呢?
答案:當一個對象被gc掉的時候通知用戶線程,進行額外的處理時,就需要使用引用隊列了。比如反向操作,其他數據清理(非GC)
* 4、能夠找到 Reference Chain 的對象,就一定會存活么?應該怎么回答呢?圖中“如果A和B引用關系不牢靠就會斷開”,怎么判斷引用關系是否牢靠?這個算法執行期間 【所有的引用關系必須都是 牢靠的】才可以嗎?
答案:不一定,還要看reference類型。弱引用會在GC時會被回收,軟引用會在內存不足的時候被回收。但沒有Reference Chain的對象就一定會被回收。
- 前言
- 開篇詞
- 基礎原理
- 第01講:一探究竟:為什么需要 JVM?它處在什么位置?
- 第02講:大廠面試題:你不得不掌握的 JVM 內存管理
- 第03講:大廠面試題:從覆蓋 JDK 的類開始掌握類的加載機制
- 第04講:動手實踐:從棧幀看字節碼是如何在 JVM 中進行流轉的
- 垃圾回收
- 第05講:大廠面試題:得心應手應對 OOM 的疑難雜癥
- 第06講:深入剖析:垃圾回收你真的了解嗎?(上)
- 第06講:深入剖析:垃圾回收你真的了解嗎?(下)
- 第07講:大廠面試題:有了 G1 還需要其他垃圾回收器嗎?
- 第08講:案例實戰:億級流量高并發下如何進行估算和調優
- 實戰部分
- 第09講:案例實戰:面對突如其來的 GC 問題如何下手解決
- 第10講:動手實踐:自己模擬 JVM 內存溢出場景
- 第11講:動手實踐:遇到問題不要慌,輕松搞定內存泄漏
- 第12講:工具進階:如何利用 MAT 找到問題發生的根本原因
- 第13講:動手實踐:讓面試官刮目相看的堆外內存排查
- 第14講:預警與解決:深入淺出 GC 監控與調優
- 第15講:案例分析:一個高死亡率的報表系統的優化之路
- 第16講:案例分析:分庫分表后,我的應用崩潰了
- 進階部分
- 第17講:動手實踐:從字節碼看方法調用的底層實現
- 第18講:大廠面試題:不要搞混 JMM 與 JVM
- 第19講:動手實踐:從字節碼看并發編程的底層實現
- 第20講:動手實踐:不為人熟知的字節碼指令
- 第21講:深入剖析:如何使用 Java Agent 技術對字節碼進行修改
- 第22講:動手實踐:JIT 參數配置如何影響程序運行?
- 第23講:案例分析:大型項目如何進行性能瓶頸調優?
- 彩蛋
- 第24講:未來:JVM 的歷史與展望
- 第25講:福利:常見 JVM 面試題補充