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對象是什麼?在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虛拟機》這本書。
視訊及電子書詳見:點這裡下載下傳
原文位址
https://www.cnblogs.com/chaogu94/p/12651920.html