垃圾收集器與內存分配策略 - 對象已死嗎

  • Java與C++之間有一堵由內存動態分配和垃圾收集技術所圍成的"高牆",牆外面的人想進去,牆裏面的人卻想出來

概述

  • 垃圾收集器(Garbage Collection)GC
  • 1960年誕生於MIT的Lisp是一門真正的使用內存動態分配和垃圾收集技術的語言
  • 思考GC須要完成的3件事:java

    • 哪些內存須要回收
    • 何時回收
    • 如何回收
  • 爲何須要了解GC和內存分配?算法

    • 但須要排查各類內存溢出、內存泄漏問題時,當垃圾收集成爲系統達到更高併發量的瓶頸時,就須要對這些「自動化」的技術實施必要的監控和調節

對象已死嗎

  • 第一件事情就是要肯定這些對象之中哪些還「存活」着,哪些已經「死去」(即不可能再被任何途徑使用的對象)

引用計數算法

  • 不少教科書判斷對象是否存活的算法:緩存

    • 給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;
    • 當引用失效時,計數器值就減1
    • 任什麼時候刻計數器爲0的對象就是不可能再被使用的
  • 引用計數法(Reference Counting)的實現簡單,斷定效率高,案例:併發

    • 微軟的COM(Component Object Model)技術
    • 使用ActionScript 3的FlashPlayer
    • Python 語言和在遊戲腳本領域被普遍應用的Squirrel
  • 主流Java虛擬機裏面沒有選用引用計數算法來管理內存。主因:它很難解決對象間相互循環引用的問題
package com.leaf.u_jvm;

/**
 * 引用計數算法的缺陷
 * 
 * testGC()方法執行後,objA、objB會不會被GC呢?
 *
 */
public class ReferenceCountingGC {

    public Object instance = null;
    
    private static final int _1MB = 1024 * 1024;
    
    /**
     * 這個成員屬性的惟一意義就是佔點內存,以便能在GC日誌中看清楚是否被回收過
     */
    private byte[] bigSize = new byte[2 * _1MB];
    
    public static void testGC(){
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        
        objA.instance = objB;
        objB.instance = objA;
        
        objA = null;
        objB = null;
        
        //假設在這行發生GC,objA和objB是否能被回收?
        System.gc();
    }
    
    public static void main(String[] args) {
        ReferenceCountingGC.testGC();
    }
}
  • 執行控制檯日誌:
[GC (System.gc()) [PSYoungGen: 5735K->824K(18944K)] 5735K->832K(62976K), 0.0012368 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 824K->0K(18944K)] [ParOldGen: 8K->693K(44032K)] 832K->693K(62976K), [Metaspace: 2593K->2593K(1056768K)], 0.0070749 secs] [Times: user=0.06 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 18944K, used 164K [0x00000000eb180000, 0x00000000ec680000, 0x0000000100000000)
  eden space 16384K, 1% used [0x00000000eb180000,0x00000000eb1a90d0,0x00000000ec180000)
  from space 2560K, 0% used [0x00000000ec180000,0x00000000ec180000,0x00000000ec400000)
  to   space 2560K, 0% used [0x00000000ec400000,0x00000000ec400000,0x00000000ec680000)
 ParOldGen       total 44032K, used 693K [0x00000000c1400000, 0x00000000c3f00000, 0x00000000eb180000)
  object space 44032K, 1% used [0x00000000c1400000,0x00000000c14ad538,0x00000000c3f00000)
 Metaspace       used 2599K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 286K, capacity 386K, committed 512K, reserved 1048576K
  • 從日誌PSYoungGen: 5735K->824K(18944K)] 5735K->832K(62976K)看虛擬機並無由於這兩個對象相互引用就不回收它們,這也從側面說明虛擬機並非經過引用計數算法判斷對象是否存活

可達性分析算法

  • 主流程序語言的主流實現,都是經過可達性分析(Reachability Analysis)來斷定對象是否存活
  • 基本思路:框架

    • 經過一系列稱爲「GC Roots」的對象做爲起始點
    • 從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain)
    • 當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的

clipboard.png

  • Java語言,可做爲GC Roots的對象包括:jvm

    • 虛擬機棧(棧幀的本地變量表)中引用的對象
    • 方法區中類靜態屬性引用的對象
    • 方法區中常量引用的對象
    • 本地方法棧中JNI(即通常說的Native方法)引用的對象

再談引用

  • 引用計數算法和可達性分析算法,斷定對象是否存活都與「引用」有關
  • JDK1.2之前,引用的定義:若是reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用
  • 一個對象在這種定義下只有被引用或者沒有被引用兩種狀態,對於描述「食之無味,棄之惋惜」的對象就顯得無能爲力
  • 但願:當內存空間還足夠時,則能保留在內存中;若是內存空間在進行垃圾收集後仍是很是緊張,則可拋棄這些對象(緩存)
    * JDK1.2以後,引用分ide

    • 強引用(Strong Reference)
    • 軟引用(Soft Reference)
    • 弱引用(Weak Reference)
    • 虛引用(Phantom Reference)
    • 引用強調依次減弱
  • 強引用,相似「Object obj = new Object()」,只要強引用存在,垃圾收集器永遠不會回收掉被引用的對象
  • 軟引用是用來描述一些還有用但並不是必需的對象,這類對象在系統將要發生內存溢出異常以前,將會把這些對象列回收範圍之中進行第二次回收。若是此次回收尚未足夠的內存,纔會拋出異常,在JDK1.2以後,提供了SoftReference類來實現軟引用
  • 弱引用也是用來描述非必需對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉被弱引用關聯的對象。在JDK1.2以後,提供了WeakReference類來實現弱引用
  • 虛引用也稱爲幽靈引用或幻影引用,是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知。在JDK1.2以後,提供了PhantomReference類來實現虛引用

生存仍是死亡

  • 即便在可達性分析算法中不可達的對象,也並不是是「非死不可」,此時暫處「緩刑」階段
  • 真正宣告一個對象死亡,至少要經歷兩次標記過程:高併發

    • 若是對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選ui

      • 篩選的條件是此對象是否有必要執行finalize()方法
      • 當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲「沒有必要執行」
      • 若是這個對象被斷定爲有必要執行finalize()方法,那麼這個對象將會放置在一個叫F-Queue的隊列之中
      • 稍後由一個虛擬機自動創建、低優先級的Finalizer線程去執行它
      • finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模的標記
      • 若是對象要在finalize()中成功拯救本身this

        • 只有從新與引用鏈上的任何一個對象創建關聯便可
        • 如把本身(this關鍵字)賦值給某個類變量或者對象的成員變量,那第二次標記時它將被移除出「即將回收」的集合
        • 若是對象這時尚未逃脫,那基本上就真的被回收了
package com.leaf.u_jvm;

/**
 * 一次自我拯救的演示
 * 1.對象能夠在被GC時自我拯救
 * 2.這種自救的機會只有一次,由於對象的finalize()方法最多隻會被系統自動調用一次
 *  正常運行結果:
 *  finalize method excuted
 *  yes, I am still alive
 *  no, I am dead
 *  
 *  finalize()方法確實執行了,可是第一次拯救成功,第二次失敗了
 *  
 *  
 * 注意:這個案例只作演示使用,切記在實際中使用,由於finalize()方法的不肯定性很大,它的優先級很低,容易受影響
 * 能夠dubug看看運行結果
 * 
 * 能夠是try-finally或者其它方式
 */
public class FinalizeEscapeGC {

    public 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 excuted");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }
    
    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();
        
        //對象第一次成功拯救本身
        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方法優先級很低,因此暫停0.5秒等待它
        Thread.sleep(500);
        if(SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, I am dead");
        }
    }
    
    
}

回收方法區

  • 不少人認爲方法區(或者HotSpot虛擬機中的永久代)是沒有垃圾收集的
  • Java虛擬機規範確實說過能夠不要求虛擬機在方法區實現垃圾收集
  • 在方法區中進行垃圾收集的「性價比」通常比較低
  • 在堆中,尤爲在新生代中,常規應用進行一次垃圾收集通常能夠回收70%-95%的空間,而永久代的垃圾收集效率遠低於此
  • 永久代的垃圾收集:廢棄常量和無用的類
  • 斷定廢棄常量

    • 回收廢棄常量與回收Java堆中的對象相似
    • 如常量池中的字面量的回收,字符串「abc」進入常量池,沒有任何String對象引用常量池中的「abc」常量,這個常量就會被系統清理出常量池
    • 常量池中的其它類(接口)、方法、字段的符號引用也相似
  • 斷定無用類

    • 該類全部的實例都已經被回收,也就是Java堆中不存在該類的任何實例
    • 加載該類的ClassLoader已經被回收
    • 該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法
  • 是否對類進行回收,HotSpot虛擬機提供了-Xnoclassgc參數進行控制

    • 可使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看類加載和卸載信息
    • 大量使用反射、動態代理、CGLib等ByteCode框架、動態生成JSP以及OSGI這類頻繁自定義ClassLoader的場景都須要虛擬機具有類卸載的功能,以保證永久代不會溢出

引用:《深刻理解Java虛擬機:JVM高級特性與最佳實踐(第2版)》 - 第三章

相關文章
相關標籤/搜索