1. 程式人生 > >Java垃圾回收(一)物件存活狀態判斷---深入理解Java虛擬機器

Java垃圾回收(一)物件存活狀態判斷---深入理解Java虛擬機器

程式計數器,虛擬機器棧和本地方法棧
首先我們先來看下垃圾回收中不會管理到的記憶體區域,在Java虛擬機器的執行時資料區我們可以看到,程式計數器,虛擬機器棧,本地方法棧這三個地方是比較特別的。這個三個部分的特點就是執行緒私有的,它們隨著執行緒的建立而誕生,也因執行緒的結束而滅亡。棧中的棧幀隨著方法的進入和退出會有條不絮的執行著進棧和出棧。每一個棧幀中分配多少記憶體,基本上是在類結構確認下來的時候就已知的,因此這幾個區域的記憶體分配和回收都具備確定性,在這幾個區域內就不需要過多考慮回收的問題,因為方法結束或者執行緒結束,記憶體自然就跟隨著回收了。

Java堆和方法區
我們討論的垃圾回收,主要就是關於Java堆中廢棄物件的回收。Java堆和方法區中,一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,我們會在程式執行的時候動態建立物件,這部分記憶體的分配和回收也是動態的,垃圾收集器所關注的是這部分記憶體如何進行回收。

物件狀態判斷
在研究物件的回收之前,我們需要先看一下如何進行判斷物件是否還有存活價值,即要先判斷物件是否還有被引用,這是我們進行垃圾回收的第一步,判斷物件存活狀態。接下來我會講一下幾種判斷的方法。

  1. 引用計數法
    一個比較通俗的方法就是當物件在建立的時候,就給物件建立一個物件計數器,每當有一個地方引用到這個物件的時候,計數器加一;當引用失效的時候,計數器減1;任何時候計數器為0的物件就是不可能被使用的,就是我們所認知的 –死亡物件。
    客觀地說,引用計數演算法的實現比較簡單,判定效率也很高,在大部分情況下它都是一個不錯的演算法,也有一些比較著名的應用案例,例如微軟公司的COM(Component Obejct Mode)技術,使用ActionScript3的FlashPlayer等技術都引用了技術演算法進行記憶體管理。
    但是,至少主流的Java虛擬機器裡面沒有用到引用計數演算法來管理記憶體,之中最主要的問題就是他很難解決物件之間相互迴圈引用的問題:
public class ReferenceGc {
    public Object instance = null;
    public static void main(String[] args){
        ReferenceGc gcA = new ReferenceGc();
        ReferenceGc gcB = new ReferenceGc();
        gcA.instance=gcB;
        gcB.instance=gcA;
        gcA=null;
        gcB=null;

        System.gc();
    }
}
    這裡面如果採用的是引用計數演算法來進行垃圾回收的話,這種物件明明是沒有使用,但是卻仍然佔記憶體,在Java中,這種情況是非常常見的,所以這種演算法並不能解決Java中物件相互引用的問題,所以Java虛擬機器判斷物件存活狀態的演算法是選擇了接下來介紹的--可達性分析演算法。

2.可達性分析演算法
可達性分析示意圖
這個演算法的基本思想是,通過一系列的GC Roots 的物件作為七點,從這些節點開始的向下搜尋,搜尋所走過的路徑成為引用連,當一個物件到GC Roots 沒有任何引用鏈相連時,則證明此物件時不可用的。
如上圖所示,雖然Object5,Object6和Object7相互有引用,但是他們與GC Roots間是不可達的,所以它們將會被判定為使可回收的物件。

大家可能會很疑惑,那為什麼Object5為什麼不能當GC Roots呢?

首先,我們要規定好GC Roots的定義範圍,並不是說所有的物件都能夠成為GC Roots:
    在Java 語言中,可作為GC Roots 的物件包括下面幾種:


     - 虛擬機器棧(棧幀中的本地變量表)引用的物件
     - 方法區中類靜態屬性引用的物件
     - 方法區中常量引用的物件
     - 本地方法棧中JNI引用的物件

物件在被回收前最後的自我救贖
上面我們說完了Java虛擬機器中判斷物件存活狀態的演算法,那麼是不是說我們判斷出物件時沒有(有效)引用之後就會被馬上回收呢?實際上並不是這樣的,即時在可達性分析分演算法中不可達的物件,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段。
物件要被宣告死亡要經歷兩個階段:
第一個階段就是可達性分析,判斷物件是否具有有效引用
第二個階段就是判斷物件是否有必要執行 finalize()方法。當物件沒有覆蓋finalize()方法或者finalize()方法已經被虛擬機器呼叫過,虛擬機器將這兩種情況稱為“沒有必要執行”,就是物件已經沒有任何機會完成救贖。
我們來看一下簡單的Demo:

package com.Shop.Test;

/**
 * Created by Administrator on 2016/7/25.
 */
public class FinalizeEscapeGc {
    private static FinalizeEscapeGc SAVE_HOOK = null;
    public void isAlive(){
        System.out.println("yes, i am still alive");
    }

    @Override
    protected  void finalize() throws Throwable{
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGc.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGc();
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if(SAVE_HOOK!= null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no ,i am dead ");
        }


        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if(SAVE_HOOK!= null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no ,i am dead ");
        }
    }
}

輸出結果:

finalize method executed!
yes, i am still alive
no ,i am dead 

我們可以看到,在物件“死亡”之後,並沒有馬上死亡,我們覆蓋了父類的finalized 方法,這時候就會給物件一次救贖的機會,但是也就僅此一次,當物件第二次“死亡”的時候,我們發現,即使是覆蓋了finalized方法也沒有辦法再去拯救這個物件了。

    從上面的例子我們可以很直白的理解這個finalized方法在垃圾回收中起到的作用,那就是一次且僅一次救贖的機會。

方法區的回收

    在上一篇文章中,我們瞭解到方法區也叫做永久代,因為進入到方法區的資料,基本上是不會死亡的,因此也就不會有垃圾回收的發生。
    實際上方法區還是會進行垃圾回收,但是效率遠遠比不上堆中垃圾的回收,因此在垃圾回收中經常會被忽略掉。方法區中,垃圾收集的主要內容分為兩部分:廢棄常量和無用的類。
  1. 廢棄常量
    這部分我們會比較容易理解,就舉一個簡單的例子,String str= “abc”;這時候abc就已經進入了常量池中,當str改變類值,那麼久沒有物件引用”abc”這個常量,這時候,”abc”就會被系統清理出常量池。常量池中的其他類、方法、欄位的符號引用也是如此

  2. 無用的類
    判定一個類是否是無用的類的條件相對苛刻許多。類需要同時滿足以下三個條件才能算是“無用的類”
    1。該類所有的例項都已經被回收,也就是Java堆中已經不存在該類的任何例項
    2。載入該類的ClassLoader已經被回收
    3。該類物件的Java.lang.Class 物件沒有在任何地方唄引用,無法再任何地方通過反射訪問該類

    虛擬機器可以對滿足上述3個條件的無用類物件進行回收,這裡說的僅僅是“可以”,行不是和物件一樣,不適用就必然回收。需要對HotSpot虛擬機器的引數進行設定,控制回收。
    一般情況下我們不會對方法區的無用類進行回收,但是在大量使用反射,動態代理、CGLib等ByteCode框架、這類頻繁自定義ClassLoader的場景都需要虛擬機器具備類解除安裝的功能,以保證永久代不會溢位。