Java虛擬機器(三)垃圾標記演算法與Java物件的生命週期

劉望舒發表於2019-01-15

相關文章
Java虛擬機器系列

前言

這一節我們來簡單的介紹垃圾收集器,並學習垃圾標記的演算法:引用計數演算法和根搜尋演算法,為了更好的理解根搜尋演算法,會在文章的最後介紹Java物件在虛擬機器中的生命週期。

1.垃圾收集器概述

垃圾收集器(Garbage Collection),通常被稱作GC。提到GC,很多人認為它是伴隨Java而出現的,其實GC出現的時間要比Java早太多了,它是1960誕生於MIT的Lisp。
GC主要做了兩個工作,一個是記憶體的劃分和分配,一個是對垃圾進行回收。關於記憶體的劃分和分配,目前Java虛擬機器記憶體的劃分是依賴於GC的的設計的,比如現在GC都是採用了分代收集演算法來回收垃圾,Java堆作為GC主要管理的區域,被細分為新生代和老年代,再細緻一點新生代又可以劃分為Eden空間、From Survivor空間、To Survivor空間等,這樣進行劃分是為了更快的進行記憶體分配和回收。空間劃分後,GC就可以為新物件分配記憶體空間。
關於對垃圾進行回收,被引用的物件是存活的物件,而不被引用的物件是死亡的物件也就是垃圾,GC要區分出存活的物件和死亡的物件,也就是垃圾標記,並對垃圾進行回收。接下來我們先來介紹垃圾標記演算法。

2.垃圾標記演算法

在對垃圾進行回收前,GC要先標記出垃圾,那麼如何標記呢,目前有兩種垃圾標記演算法,分別是引用計數演算法和根搜尋演算法,這兩個演算法都和引用有些關聯,因此講垃圾標記演算法前,我們先回顧下引用的知識。

引用

在JDK1.2之後,Java將引用分為強引用、軟引用、弱引用和虛引用。

  • 強引用:當我們new一個物件時就是建立了一個具有強引用的物件,如果一個物件具有強引用,垃圾收集器就絕不會回收它。Java虛擬機器寧願丟擲OutOfMemoryError異常,使程式異常終止,也不會回收具有強引用的物件來解決記憶體不足的問題。
  • 軟引用:如果一個物件只具有軟引用,當記憶體不夠時,會回收這些物件的記憶體,回收後如果還是沒有足夠的記憶體,就會丟擲OutOfMemoryError異常。Java提供了SoftReference類來實現軟引用。
  • 弱引用:弱引用比起軟引用具有更短的生命週期,垃圾收集器一旦發現了只具有弱引用的物件,不管當前記憶體是否足夠,都會回收它的記憶體。Java提供了WeakReference類來實現弱引用。
  • 虛引用:虛引用並不會決定物件的生命週期,如果一個物件僅持有虛引用,這就和沒有任何引用一樣,在任何時候都可能被垃圾收集器回收。一個只具有虛引用的物件,被垃圾收集器回收時會收到一個系統通知,這也是虛引用的主要作用。Java提供了PhantomReference類來實現虛引用。

引用計數演算法

引用計數演算法的基本思想就是每個物件都有一個引用計數器,當物件在某處被引用的時候,它的引用計數器就加1,引用失效時就減1。當引用計數器中的值變為0,則該物件就不能被使用成了垃圾。
目前主流的Java虛擬機器沒有選擇引用計數演算法來為垃圾標記,主要原因是引用計數演算法沒有解決物件之間相互迴圈引用的問題。
舉個例子,下面程式碼的註釋1和註釋2處,d1和d2相互引用,除此之外這兩個物件無任何其他引用,實際上這兩個物件已經死亡,應該作為垃圾被回收,但是由於這兩個物件互相引用,引用計數就不會為0,垃圾收集器就無法回收它們。

class _2MB_Data {
    public Object instance = null;
    private byte[] data = new byte[2 * 1024 * 1024];//用來佔記憶體,測試垃圾回收
}

public class ReferenceGC {
    public static void main(String[] args) {
        _2MB_Data d1 = new _2MB_Data();
        _2MB_Data d2 = new _2MB_Data();
        d1.instance = d2;//1
        d2.instance = d1;//2
        d1 = null;
        d2 = null;
        System.gc();
    }
}複製程式碼

如果你使用Android Studio,就在Edit Configurations中的VM options加入如下語句來輸出詳細的GC日誌:

-XX:+PrintGCDetails複製程式碼

執行程式,GC日誌為:
[GC (System.gc()) [PSYoungGen: 8028K->832K(76288K)] 8028K->840K(251392K), 0.0078334 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 832K->0K(76288K)] [ParOldGen: 8K->603K(175104K)] 840K->603K(251392K), [Metaspace: 3015K->3015K(1056768K)], 0.0045844 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 76288K, used 1966K [0x000000076af80000, 0x0000000770480000, 0x00000007c0000000)
eden space 65536K, 3% used [0x000000076af80000,0x000000076b16bac0,0x000000076ef80000)
from space 10752K, 0% used [0x000000076ef80000,0x000000076ef80000,0x000000076fa00000)
to space 10752K, 0% used [0x000000076fa00000,0x000000076fa00000,0x0000000770480000)
ParOldGen total 175104K, used 603K [0x00000006c0e00000, 0x00000006cb900000, 0x000000076af80000)
object space 175104K, 0% used [0x00000006c0e00000,0x00000006c0e96d10,0x00000006cb900000)
Metaspace used 3046K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 334K, capacity 388K, committed 512K, reserved 1048576K

檢視此GC日誌前我們先來簡單瞭解下各引數的含義,[GC (System.gc()和[Full GC (System.gc()說明了這次垃圾收集的停頓型別,而不是來區分新生代GC和老年代GC的。 [Full GC (System.gc() 說明這次GC發生了STW,STW也就是Stop the World機制,意思是說在執行垃圾收集演算法時,只有GC執行緒在執行,其他的執行緒則會全部暫停,等待GC執行緒執行完畢後才能再次執行。
PSYoungGen代表新生代,ParOldGen代表老年代,Metaspace代表元空間(JDK 8中用來替代永久代PermGen)。
我們來看日誌的[GC (System.gc()),記憶體變化為:8028K->840K(251392K),8028K代表回收前的記憶體大小,840K代表回收後的記憶體大小,251392K代表記憶體總大小。因此可以得知記憶體回收大小為(8028-840)K。這就說明JDK8的HotSpot虛擬機器並沒有採用引用計數演算法來標記記憶體,它對上述程式碼中的兩個死亡物件的引用進行了回收。

根搜尋演算法

這個演算法的基本思想就是選定一些物件作為GC Roots,並組成根物件集合,然後從這些作為GC Roots的物件作為起始點,向下進行搜尋,如果目標物件到GC Roots是連線著的,我們則稱該目標物件是可達的,如果目標物件不可達則說明目標物件是可以被回收的物件,如下圖所示。

Java虛擬機器(三)垃圾標記演算法與Java物件的生命週期
未命名檔案.png

從上圖看以看出,Obj5、Obj6和Obj7都是不可達的物件,其中Obj5和Obj6雖然互相引用,但是因為他們到GC Roots是不可達的所以它們仍舊會判定為可回收的物件,這樣根搜尋演算法就解決了引用計數演算法無法解決的問題:已經死亡的物件因為相互引用而不能被回收。
在Java中,可以作為GC Roots的物件主要有以下幾種:

  • Java棧中的引用的物件。
  • 本地方法棧中JNI引用的物件。
  • 方法區中執行時常量池引用的物件。
  • 方法區中靜態屬性引用的物件。
  • 執行中的執行緒
  • 由引導類載入器載入的物件
  • GC控制的物件

還有一個問題是被標記為不可達的物件會立即被垃圾收集器回收嗎?要回答這個問題我們首先要了解Java物件在虛擬機器中的生命週期。

3.Java物件在虛擬機器中的生命週期

當Java物件被類載入器載入到虛擬機器中後,Java物件在Java虛擬機器中有7個階段。
1.建立階段(Created)
建立階段的具體步驟為:

  • 為物件分配儲存空間。
  • 構造物件。
  • 從超類到子類對static成員進行初始化。
  • 遞迴呼叫超類的構造方法。
  • 呼叫子類的構造方法。

2.應用階段(In Use)
當物件被建立,並分配給變數賦值,狀態就切換到了應用階段。
這一階段的物件至少要具有一個強引用,或者顯式的使用軟引用、弱引用或者虛引用。

3.不可見階段(Invisible)
程式中找不到物件的任何強引用,比如程式的執行已經超出了該物件的作用域。在不可見階段,物件仍可能被特殊的強引用GC Roots持有著,比如物件被本地方法棧中JNI引用或是被執行中的執行緒引用等。

4.不可達階段(Unreachable)
程式中找不到物件的任何強引用,並且垃圾收集器發現物件不可達。

5.收集階段(Collected)
垃圾收集器已經發現物件不可達,並且垃圾收集器已經準備好要對該物件的記憶體空間重新進行分配時。這個時候如果該物件重寫了finalize方法,則會呼叫該方法。

6.終結階段(Finalized)
當物件執行完finalize法後仍然處於不可達狀態時,或者物件沒有重寫finalize方法,則該物件進入終結階段,並等待垃圾收集器回收該物件空間。

7.物件空間重新分配階段(Deallocated)
當垃圾收集器對物件的記憶體空間進行回收或者再分配時,這個物件就會徹底消失。

好了,我們已經瞭解了Java物件在虛擬機器中的生命週期,再來回想我方才說的問題:被標記為不可達的物件會立即被垃圾收集器回收嗎?很顯然是不會的,被標記為不可達的物件會進入收集階段,這時會執行該物件重寫的finalize方法,如果沒有重寫finalize方法或者finalize方法中沒有重新與一個可達的物件進行關聯才會進入終結階段,並最終被回收。

參考資料
《深入理解 Java 虛擬機器:JVM 高階特性與最佳實踐》第二版
《Java虛擬機器精講》
《HotSpot實戰》
《Android應用效能優化最佳實踐》
JVM 深入筆記(3)垃圾標記演算法
GC roots
Java GC - 監控回收行為與日誌分析
Java:物件的強、軟、弱和虛引用
JVM GC中Stop the world案例實戰
Java物件的生命週期


歡迎關注我的微信公眾號,第一時間獲得部落格更新提醒,以及更多成體系的Android相關原創技術乾貨。
掃一掃下方二維碼或者長按識別二維碼,即可關注。

Java虛擬機器(三)垃圾標記演算法與Java物件的生命週期

相關文章