1. 程式人生 > >JVM(二) -- 垃圾回收

JVM(二) -- 垃圾回收

所在 ade 退出 throwable noclass test 根搜索算法 protect !=

在前一篇大致講解了JVM的內存結構,在對JVM有一定了解的基礎上,接下來進行JVM垃圾收集的學習

垃圾收集器與內存分配策略

1.概述

  內存的動態分配與內存回收技術已經很成熟了,了解GC和內存分配:一方面為了當出現內存溢出,內存泄漏的時候排查問題,另一方面垃圾收集會成為實現更高並發量的瓶頸,所以我們需要對這些“自動化”的技術實施必要的監控和調節。

對於程序計數器、虛擬機棧、本地方法棧這三個內存區域隨線程而生,隨線程而滅;棧中的棧幀隨著方法的進入和退出而有條不紊地執行著進棧和出棧的操作,線程結束則內存被回收。所以線程私有的內存區域是不需要額外控制的。Java堆和方法區就不一樣了,我們只有程序處於運行期間時才知道(多態導致)創建哪些對象,這部分的內存分配和回收都是動態的。內存的分配和回收指的就是Java堆和方法區部分的內存

3.2 回收堆中死亡對象

堆中幾乎存放著Java世界中所有的對象實例,垃圾收集器回收的內存就是那些已經死了的對象實例的內存。對象已死表示不可能再被任何途徑使用的對象。   下面介紹幾種判斷對象是否存活的算法:

3.2.1 引用計數法

引用計數法判斷對象存活的通過給對象添加一個引用計數器(在new Object時候初始化計數器為0),每當有一個地方引用(例如,在Object obj = new Object()時,計數器加1)它時,計數器就加1;當引用失效時(例如在方法中局部變量引用了對象,方法執行完成時,就是引用失效),計數器值就減1;任何時刻計數器都為0的對象就是不可能再被使用的
。 引用計數法實際上是一個效率很高的算法,實現也簡單,但是卻存在一個隱患,對象之間的相互循環引用會導致內存泄漏【內存泄漏這裏指的是該內存應該被但沒有被回收,導致內存一直被占用】。 如下情況:Java 代碼   
/**
* testGC 方法執行後,objA和objB會不會被GC回收呢?
*/
public class TestGC{
    public Object instance = null;
    
    public static void testGC(){
        TestGC objA = new TestGC();
        TestGC objB 
= new TestGC(); objA.instance = objB; objB.instance = objA; //圖一 objA = null; objB = null; //圖二 } //圖三 }
如下的幾張圖正是上面Java代碼的內存,在testGC方法執行時,objA和objB為局部變量分別進棧,並且指向堆中的對象實例。             圖一 實際上應該Java棧到objB為止,沒有別的變量進棧,下同             圖中的引用計數器為2 技術分享              圖二 圖中的引用計數器為1 技術分享 圖三 圖中的引用計數器為1 技術分享 上面的圖示解釋了,引用計數法的缺陷,會導致內存泄漏。

3.2.2 根搜索算法

根搜索算法是目前的流行的判斷對象算法存活的算法。 此算法的思路是通過一系列的名為“GCRoots”的對象作為起始點,從這些根節點開始向下搜索,搜索所走過的路徑成為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(其實就是數據結構中的圖來解釋就是,GC Roots到該對象不可達)時。則證明此對象是不可用的。 下圖中的Object5,6,7會被回收,在上一例中的圖二和圖三中的objA和objB就會被回收     技術分享

3.2.3 Java對引用的改進

在JDK 1.2以前,引用定義很傳統;如果reference類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就成這塊內存代表著一個引用。 JDK1.2之後對引用類型進行了擴充,將引用分為強引用,軟引用,弱引用,虛引用 強引用是指類似於 Object obj = new Object()這類的引用,其實也就是直接存在於Java棧中的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象; 軟引用是指一些還有用但是不是必需的對象,這些對象會進行二次回收,也就是在內存塊溢出的時候,將軟引用進行標記,然後進行第一次回收,但是軟引用對象沒有被回收,如果第一次回收完成後,還是沒有足夠的內存,那麽才會回收標記的軟引用對象弱引用是指一些非必需的對象,在下一次垃圾回收時就會被回收。 虛引用更弱,回收他們不會造成任何影響。

3.2.4 根搜索的生存還是死亡(覆蓋finalize方法可以拯救一次)

對於根搜索算法,當在發現對象不可達GC Roots時,並不是立即把他們回收了,先判斷是否需要執行finalize()方法【註:如果對象沒有覆蓋finalize()方法,或者finalize方法已經被虛擬機執行過一次了,那麽就視為沒必要執行finalize()方法了,直接進行回收】。 如果判斷需要執行finalize()方法,那麽這個對象會被放在一個F-Queue的隊列中,處於F-Queue隊列中的對象有一次逃脫死亡命運的機會,如果finalize()方法中只要把對象重新和reference chain(GC Roots所在的的圖)鏈接上,那麽就成功拯救了自己。譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量。 請用javac -className.java java -className 來執行下面的文件,查看根搜索算法中的自我拯救。
/**
    下面的代碼演示了兩點:
    1.對象可以在GC時自我拯救
    2.這種自救的機會只有一次,因為一個對象的finalize方法最多只會被系統自動的執行一次
*/
public class EscapeGC{
    public static EscapeGC 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");
        EscapeGC.SAVE_HOOK = this;//在這裏吧自己賦值給了類變量,拯救了自己
    }
    
    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new EscapeGC();
        
        //對象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        //因為Finalize方法優先級很低,暫停0.5秒,等待它
        Thread.sleep(500);
        if(SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no, i am dead :(");
        }
        
        //下面這段代碼與上面的完全相同,但是這次自救卻失敗了
        SAVE_HOOK = null;
        System.gc();//一個對象的finalize方法最多會執行一次
        //因為Finalize方法優先級很低,暫停0.5秒,等待它
        Thread.sleep(500);
        if(SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no, i am dead :(");
        }
        
    }
    
    
    
    
}

在介紹完根搜索算法後,堆中對象的死亡判斷算法已經介紹完了。接下來介紹一下方法區中的垃圾回收。

3.2.5 回收方法區

方法區(HotSpot虛擬機中的永久代)在Java虛擬機規範中提到,不要求虛擬機在方法區實現垃圾收集,而且方法區的垃圾收集“性價比”很低而在堆中,尤其是新生代中,進行一次垃圾收集一般可以回收70%~95%的空間,而方法區的垃圾收集效率遠低於此。 方法區的垃圾收集主要回收兩部分內容:廢棄常量無用的類。回收廢棄的常量和回收Java堆中的對象很類似【例如,字符串“abc”已經進入常量池,在內存回收時,發現沒有任何的String對象引用常量池中的“abc”常量,那麽這個時候“abc”會被系統清理出常量池】,常量池中包括類(接口)、方法、字段的符號引用【參考後面的類文件結構篇】 判定一個類是否是“無用的類”相對復雜,需要滿足如下條件:   1:該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例【因為對象實例的數據有兩部分組成:對象實例數據(Java堆中),對象類型數據(方法區中)】。 技術分享   2:加載該類的Classloader已經被回收【這個還不知道為什麽,需要在學習類加載機制之後來補充】。 3:該類對應的java.long.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法【也就是該類的方法都是不能通過反射機制訪問的】。 就算滿足上面的條件,HotSpot也可以選擇不回收這些“無用的類”,要想回收無用的類,需要-Xnoclassgc參數進行控制,如前面所說的,方法區的垃圾回收是可以選擇的。 而對於大量使用反射、動態代理、GCLib等bytecode框架的場景以及動態生成JSP的頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代(方法區)不會溢出

3.3 垃圾收集算法

3.3.1 標記-清除算法

待補充

3.3.2 復制算法

待補充

3.3.3 標記-整理算法

待補充

3.3.4 分代收集算法

待補充

JVM(二) -- 垃圾回收