JVM中垃圾回收機制如何判斷是否死亡?詳解引用計數法和可達性分析 !

因為熱愛,所以堅持。
文章下方有本文參考電子書和視頻的下載地址哦~

這節我們主要講垃圾收集的一些基本概念,先了解垃圾收集是什麼、然後觸發條件是什麼、最後虛擬機如何判斷對象是否死亡。

一、前言

  我們都知道Java和C++有一個非常大的區別就是Java有自動的垃圾回收機制,經過半個多世紀的發展,Java已經進入了「自動化」時代,讓使用者只需要注重業務邏輯的開發而不需要擔心內存的使用情況。那麼我們為什麼還要學習Java的垃圾回收機制呢?原因很簡單:我們不想止於「增刪改查工程師」這樣的初級水平,一旦程序發生了內存溢出、內存泄漏等問題時,我們可以用已掌握的知識更好的調節和優化我們的代碼。在學這章節之前,默認大家已經了解並掌握了Java內存運行時的五個區域的功能:方法區、Java堆、虛擬機棧、本地方法棧、程序計數器。還沒有了解過的朋友請先看這裡:JVM中五大內存區域

二、判斷對象是否死亡

客官們可以先想一下,GC(垃圾回收機制)在清理內存的時候第一件事要做什麼?肯定是要先判斷內存中的對象是否已經死亡,也就是再也不會被使用了,然後才會去回收這些對象。判斷對象是否死亡通常會有兩種辦法:引用計數法可達性分析

2.1 引用計數法

使用引用計數法,要先給每一個對象中添加一個計數器,一旦有地方引用了此對象,則該對象的計數器加1,如果引用失效了,則計數器減1。這樣當計數器為0時,就代表此對象沒有被任何地方引用。這種方法實現簡單,判定效率也很高,在大部分情況下都是一個比較不錯的方法。但是在Java虛擬機中並沒有選用引用計數法來管理內存,其主要原因是它很難解決對象之間相互引用的問題,如果兩個對應互相引用,導致他們的引用計數都不為0,最終不能回收他們。我們來舉個例子

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 圖解
理解了可達性分析法,你可能又會問了GC root對象是什麼?在JAVA語言中,可以作為GC root的對象包括以下幾種:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  • 方法區中類靜態屬性引用的對象。
  • 方法區中常量引用的對象。
  • 本地方法棧中JNI(Java Native Interface)引用的對象。

以上四種不需要死記硬背,由於方法區、虛擬機棧和本地方法棧中保存了類中和方法中定義的變量的引用,既然是自己定義的變量,所以肯定是有用的。

2.4 「引用」是什麼

我們知道java中將數據類型分為兩大類:基本類型和引用類型。java中引用的定義是:如果reference類型的數據中存儲的數值代表的是另一塊內存的起始地址,就稱這塊內存代表着一個引用。舉個例子:

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賦值給某個類的變量,或者對象的成員變量,那麼在第二次標記時它將被移除「即將回收」的集合,下面我們看一個案例來了解。

/**   * @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,我哏兒屁了");          }      }    }  

運行結果:
自我拯救運行結果
注意:根據《深入理解Java虛擬機》中解釋這種自我拯救的方法運行代價高昂,不確定性大,無法保證各個對象的調用順序,因此這一知識點僅作了解即可。

2.6 回收方法區

  由於我們經常用的HotSpot虛擬機規定方法區也可以稱為永久代,因此很多人認為在方法區中是沒有垃圾收集的,其實是有的,只不過收集垃圾的「性價比」非常低。在堆中,尤其是新生代,垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠低於此。永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。

  • 回收廢棄常量:當前系統中沒有任何對象引用常量池中的某個常量,則一旦發生內存回收,如果有必要,該常量就會被系統清理出常量池。
  • 回收無用的類:要滿足三個條件才能證明某個類是無用的,1.類的實例都已經被回收了。2.加載該類的ClassLoader也被回收了。3.該類對應的java.lang.Class對象沒有在任何地方被引用。注意:滿足以上三點的類只是說可以被回收,但並不像對象一樣一定會被回收,是否進行回收可以使用虛擬機提供的參數來控制。大量使用反射、動態代理等頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載功能,以保證永久代不會溢出。

本博客參考《深入理解Java虛擬機》這本書。
視頻及電子書詳見:點這裡下載