JVM中垃圾回收機制如何判斷是否死亡?詳解引用計數法和可達性分析 !
阿新 • • 發佈:2020-04-07
> 因為熱愛,所以堅持。
> 文章下方有本文參考電子書和視訊的**下載地址**哦~
這節我們主要講垃圾收集的一些基本概念,先了解垃圾收集是什麼、然後觸發條件是什麼、最後虛擬機器如何判斷物件是否死亡。
### 一、前言
我們都知道Java和C++有一個非常大的區別就是Java有自動的垃圾回收機制,經過半個多世紀的發展,Java已經進入了“自動化”時代,讓使用者只需要注重業務邏輯的開發而不需要擔心記憶體的使用情況。那麼我們為什麼還要學習Java的垃圾回收機制呢?原因很簡單:我們不想止於“增刪改查工程師”這樣的初級水平,一旦程式發生了記憶體溢位、記憶體洩漏等問題時,我們可以用已掌握的知識更好的調節和優化我們的程式碼。在學這章節之前,預設大家已經瞭解並掌握了Java記憶體執行時的五個區域的功能:方法區、Java堆、虛擬機器棧、本地方法棧、程式計數器。還沒有了解過的朋友請先看這裡:[JVM中五大記憶體區域](https://www.cnblogs.com/chaogu94/p/12529692.html)
### 二、判斷物件是否死亡
客官們可以先想一下,GC(垃圾回收機制)在清理記憶體的時候第一件事要做什麼?肯定是要先判斷記憶體中的物件是否已經死亡,也就是再也不會被使用了,然後才會去回收這些物件。判斷物件是否死亡通常會有兩種辦法:**引用計數法**和**可達性分析**。
#### 2.1 引用計數法
使用引用計數法,要先給每一個物件中新增一個計數器,一旦有地方引用了此物件,則該物件的計數器加1,如果引用失效了,則計數器減1。這樣當計數器為0時,就代表此物件沒有被任何地方引用。這種方法實現簡單,判定效率也很高,在大部分情況下都是一個比較不錯的方法。但是在Java虛擬機器中並沒有選用引用計數法來管理記憶體,其主要原因是它很難解決物件之間相互引用的問題,如果兩個對應互相引用,導致他們的引用計數都不為0,最終不能回收他們。我們來舉個例子
```java
class Person{
public Person lover = null;//定義一個愛人
private String name = "";//姓名
Person(String name){
this.name = name;
}
}
public class Demo {
public static void main(String[] args) {
Person liangshanbo = new Person("梁山伯");//建立一個人物:梁山伯
Person zhuyingtai = new Person("祝英臺");//建立一個人物:祝英臺
liangshanbo.lover = zhuyingtai;//設定梁山伯的愛人是祝英臺
zhuyingtai.lover = liangshanbo;//設定祝英臺的愛人是梁山伯
}
}
```
其中梁山伯和祝英臺兩個物件互相引用,因此如果使用引用計數法來判斷物件是否死亡的話,垃圾回收機制是不能回收這兩個物件的。
#### 2.2 可達性分析演算法
在大部分主流語言中都是通過此方法來判斷物件是否存活的,這個演算法的思想是通過一系列被稱為“GC root”的物件作為起始點,從這些節點開始向下搜尋,走過的路徑叫做引用鏈。如果一個物件沒有通過引用鏈連線到GC root節點,則證明此物件是不可用的,如下圖所示,GC roots 是根節點,凡是能通過引用鏈連線上GC root 的Object 1,2,3,4都是被使用的物件。但是Object 5,6,7卻不能通過任何方式連線上根節點,因此判定Object 5,6,7為可回收的節點。
![GC root 圖解](https://user-gold-cdn.xitu.io/2020/4/6/1714e25185968f09?w=616&h=411&f=png&s=23036)
理解了可達性分析法,你可能又會問了GC root物件是什麼?在JAVA語言中,可以作為GC root的物件包括以下幾種:
- 虛擬機器棧(棧幀中的本地變量表)中引用的物件。
- 方法區中類靜態屬性引用的物件。
- 方法區中常量引用的物件。
- 本地方法棧中JNI(Java Native Interface)引用的物件。
以上四種不需要死記硬背,由於方法區、虛擬機器棧和本地方法棧中儲存了類中和方法中定義的變數的引用,既然是自己定義的變數,所以肯定是有用的。
#### 2.4 “引用”是什麼
我們知道java中將資料型別分為兩大類:基本型別和引用型別。java中引用的定義是:如果reference型別的資料中儲存的數值代表的是另一塊記憶體的起始地址,就稱這塊記憶體代表著一個引用。舉個例子:
```java
Person p = new Person();
```
上面程式碼的寫法我們經常見到,其中等號後面的 **new Person();** 是真正的物件,所有的內容都儲存在java堆記憶體中,而等號前面的 **p** 只是真實內容的一個代稱,儲存在虛擬機器棧記憶體中,它儲存的只是一個地址,是 **new Person();** 在堆記憶體中的起始位置,因此 **p** 就是一個引用。
按照這種理解,java的物件只能夠分為被引用和沒有被引用兩種情況。但是在JDK1.2之後,java對引用的概念進行了擴充,分為強、軟、弱、虛四種引用,且強度依次逐漸降低。
- 強引用:即咱們經常看到的引用方式,如在方法中定義:Object obj = new Object();,真正的物件“new Object()”儲存在java堆中,其中“obj”代表了一個引用,存放的是java堆中“new Object()”的起始地址。只要引用還在,垃圾收集器就不會回收掉被引用的物件。
- 軟引用:是用來描述一些有用但非必須的物件,我們可以使用SoftReference類來實現軟引用。對於軟引用關聯著的物件,在系統將要發生記憶體溢位異常之前,會把這些物件列進回收範圍之中。如果回收之後記憶體還是不足,才會報記憶體溢位的異常。
- 弱引用:是用來描述非必須的物件,使用WeakReference類來實現弱引用。它只能生存到下一次垃圾回收發生之前,當垃圾回收機制開始時,無論是否會記憶體溢位,都將回收掉被弱引用關聯的物件。
- 虛引用:最沒有存在感的一種引用關係,可以通過PhantomReference類來實現。存在不存在幾乎沒影響,也不能通過虛引用來獲取一個物件例項,存在的唯一目的是被垃圾收集器回收後可以收到一條系統通知。
我們可以通過程式碼來控制物件的“強軟弱虛”四種引用,有利於JVM進行垃圾回收。那麼知道了上面的知識後,我們來探究一下物件是否會死亡?
#### 2.5 物件是否死亡
之前提到過,通過可達性分析後,找到的不可達物件會被垃圾收集器回收,那麼,不可達物件一定會被回收嗎?答案是不一定。這時候他們處於“死緩”的階段,如果非要“上訴”,也是有可能被無罪釋放的。他們是如何自救的?在可達性分析後發現一些物件沒有跟GC root相連線的引用鏈,該物件會被進行一次標記,然後進行篩選,篩選的條件是判斷該物件有沒有必要執行finalize()方法(此方法每個物件預設都有),但如果物件沒有重寫finalize()方法或者物件的finalize方法已經被虛擬機器呼叫過一次了,則都將視為“沒有必要執行”,垃圾回收器可以直接回收。
(此段是自我拯救的過程,不是重點了解即可)如果該物件被判定有必要執行finalize()方法,那麼虛擬機器會把這個物件放置在一個F-Queue的佇列中,然後由一個專門的Finalizer執行緒去執行這個物件的finalize()方法。我們可以在這個方法中進行物件的“自我拯救”,即重新與引用鏈上的任何一個物件建立關聯就可以了,比如把this賦值給某個類的變數,或者物件的成員變數,那麼在第二次標記時它將被移除“即將回收”的集合,下面我們看一個案例來了解。
```java
/**
* @author 程式設計開發分享者
* @Date 2020/3/16 10:51
*/
public class FinalizeEscapeGC {
/**
* 知識點回顧:
* 1.方法區中存放的是類的基本資訊、靜態變數、編譯後的程式碼、常量池
* 2.GC root可以是方法區中靜態變數引用的物件
* 3.一個物件的finalize()方法最多隻會被系統自動呼叫一次。
* */
//建立一個靜態變數
public static FinalizeEscapeGC SAVE_HOOK = null;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("程式執行了finalize()方法");
SAVE_HOOK = this;//將自己賦值給一個靜態變數實現自我拯救,連線上了GC root(細品知識點回顧)
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
//第一次準備殺死物件
SAVE_HOOK = null;//將物件置空,按理說會被GC回收,但此物件實現了finalize()方法並實現了自我拯救
System.gc();//執行GC
Thread.sleep(500);//由於Finalizer執行緒優先順序比較低,因此短暫休眠主執行緒等等它
if (SAVE_HOOK!=null){
System.out.println("哈哈哈,我還活著");
}else {
System.out.println("No,我哏兒屁了");
}
System.out.println("--------------------------");
//第二次準備殺死物件(跟上面程式碼一樣)
SAVE_HOOK = null;//將物件置空,此時finalize()方法已經自動執行過一次了
System.gc();//執行GC
Thread.sleep(500);//由於Finalizer執行緒優先順序比較低,因此短暫休眠主執行緒等等它
if (SAVE_HOOK!=null){
System.out.println("哈哈哈,我還活著");
}else {
System.out.println("No,我哏兒屁了");
}
}
}
```
執行結果:
![自我拯救執行結果](https://user-gold-cdn.xitu.io/2020/4/6/1714e251860aa074?w=369&h=165&f=png&s=27139)
**注意:根據《深入理解Java虛擬機器》中解釋這種自我拯救的方法執行代價高昂,不確定性大,無法保證各個物件的呼叫順序,因此這一知識點僅作了解即可。**
#### 2.6 回收方法區
由於我們經常用的HotSpot虛擬機器規定方法區也可以稱為永久代,因此很多人認為在方法區中是沒有垃圾收集的,其實是有的,只不過收集垃圾的“價效比”非常低。在堆中,尤其是新生代,垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠低於此。永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。
- 回收廢棄常量:當前系統中沒有任何物件引用常量池中的某個常量,則一旦發生記憶體回收,如果有必要,該常量就會被系統清理出常量池。
- 回收無用的類:要滿足三個條件才能證明某個類是無用的,1.類的例項都已經被回收了。2.載入該類的ClassLoader也被回收了。3.該類對應的java.lang.Class物件沒有在任何地方被引用。注意:滿足以上三點的類只是說可以被回收,但並不像物件一樣一定會被回收,是否進行回收可以使用虛擬機器提供的引數來控制。大量使用反射、動態代理等頻繁自定義ClassLoader的場景都需要虛擬機器具備類解除安裝功能,以保證永久代不會溢位。
> 本部落格參考《深入理解Java虛擬機器》這本書。
視訊及電子書詳見:[點這裡下載](https://shimo.im/docs/HP6qqHx38x