Java虛擬機(三)垃圾標記算法與Java對象的生命週期

相關文章
Java虛擬機系列javascript

前言

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

1.垃圾收集器概述

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

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日誌:jsp

-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是鏈接着的,咱們則稱該目標對象是可達的,若是目標對象不可達則說明目標對象是能夠被回收的對象,以下圖所示。
測試

未命名文件.png

從上圖看以看出,Obj五、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相關原創技術乾貨。
掃一掃下方二維碼或者長按識別二維碼,便可關注。

相關文章
相關標籤/搜索