JVM之判斷對象是否存活的算法和相關知識

JVM之判斷對象是否存活的算法和相關知識

垃圾回收器相關概述

Java內存運行時數據區域的各個部分,其中程序計數器,虛擬機棧,本地方法棧三個區域隨線程而生,隨線程而滅;棧中
的棧幀隨着方法的進入和退出而有條不紊的進行出棧和入棧操作。每一個棧幀中分配多少內存基本上是在類結構確定下來時就是已知的,因此這幾個區域的內存分配和回收都具有確定性,在這幾個區域內不需要過多的考慮回收的問題,因爲方法結束或者線程結束時,內存就自然的跟着回收了

而java堆和方法區則不同,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存可能也不一樣,我們只有在程序處於運行期間才能知道會創建哪些對象,這部分內存的分配和回收都是動態的,垃圾收集器關注的就是這部分的內存

堆中幾乎存放者java所有的對象實例,垃圾收集器在堆堆進行回收前,第一件事就是確定這些對象有哪些還“存活”着,哪些已經“死去”(即不可能再被任何途徑使用的對象)

判斷對象是否存活的算法

  • 引用計數算法

    定義:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加一;當引用失效時,計數器值就減一;在任何時刻計數器爲零的對象就是不可能再被使用的

    客觀地說,引用計數算法的實現簡單,判定效率也很高,在大部分情況下都是一個不錯的算法,有比較著名的應用案例,例如微軟的COM技術,使用ActionScript 3的FlashPlayer,Python語言以及在遊戲腳本領域中被廣泛應用的Squirrel中都使用了引用計數算法進行內存管理。但是在java語言中沒有選用引用計數算法來管理內存,其中最主要的原因是它很難解決對象之間的相互循環引用的問題。

    例如以下demo,其中的testCG()方法:對象objA和objB都有字段instance,賦值令objA.instance=objB以及objB.instance=objA,除此之外,這兩個對象再無任何其他引用,實際上這兩個對象都已經不可能再被訪問,但是因爲它們互相引用着對方,導致它們的引用計數都不爲0,於是引用計數算法無法通知GC收集器回收它們

    package com.lagoon.test;
    
    /**
     * @Author WinkiLee
     * @Date 2019/5/9 21:03
     * @Description 描述在垃圾回收機制中,引用計數算法的缺陷
     */
    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;
    
            System.gc();
        }
    
        public static void main(String[] args) {
            testGC();
        }
    
    }
    

設置vm命令參數

-XX:+PrintGCDetails

接着啓動和運行程序控制臺打印GC日誌信息

"C:\Program Files\Java\jdk1.8.0_201\bin\java.exe" -XX:+PrintGCDetails "-javaagent:G:\IDEA\IntelliJ IDEA 2019.1\lib\idea_rt.jar=64569:G:\IDEA\IntelliJ IDEA 2019.1\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_201\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\rt.jar;F:\IDEA工作區\垃圾回收算法之引用計數算法的缺陷demo\out\production\垃圾回收算法之引用計數算法的缺陷demo;D:\MAVEN\Localres\org\junit\jupiter\junit-jupiter-api\5.5.0-M1\junit-jupiter-api-5.5.0-M1.jar;D:\MAVEN\Localres\org\apiguardian\apiguardian-api\1.0.0\apiguardian-api-1.0.0.jar;D:\MAVEN\Localres\org\opentest4j\opentest4j\1.1.1\opentest4j-1.1.1.jar;D:\MAVEN\Localres\org\junit\platform\junit-platform-commons\1.5.0-M1\junit-platform-commons-1.5.0-M1.jar" com.lagoon.test.ReferenceCountingGC
[GC (System.gc()) [PSYoungGen: 8044K->712K(57344K)] 8044K->720K(188416K), 0.0008123 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 712K->0K(57344K)] [ParOldGen: 8K->642K(131072K)] 720K->642K(188416K), [Metaspace: 3215K->3215K(1056768K)], 0.0040893 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 57344K, used 491K [0x0000000780980000, 0x0000000784980000, 0x00000007c0000000)
  eden space 49152K, 1% used [0x0000000780980000,0x00000007809faf88,0x0000000783980000)
  from space 8192K, 0% used [0x0000000783980000,0x0000000783980000,0x0000000784180000)
  to   space 8192K, 0% used [0x0000000784180000,0x0000000784180000,0x0000000784980000)
 ParOldGen       total 131072K, used 642K [0x0000000701c00000, 0x0000000709c00000, 0x0000000780980000)
  object space 131072K, 0% used [0x0000000701c00000,0x0000000701ca0b08,0x0000000709c00000)
 Metaspace       used 3222K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

可以看出兩個對象作爲新生代對象被回收了,虛擬機並沒有因爲這兩個對象互相引用就不回收它們,這就從側面說明了java虛擬機並不是通過引用計數算法來判斷對象是否存活的

有關GC VM參數設置和日誌分析可參考

【GC分析】Java GC日誌查看

java之GC日誌該怎麼看

接下來用可視化的GC日誌分析工具再次查看日誌表達的信息

首先設置VM命令

-Xloggc:F:/logs/gc.log 日誌文件的輸出路徑 前提得有logs這個文件夾

得到GC日誌信息打開如下:

進入網站GC日誌在線可視化分析 http://gceasy.io/

選擇剛纔的日誌文件上傳點擊開始分析


排除計數算法的缺陷外,模擬一下計數算法堆垃圾回收機制的實現

以人吃蘋果爲例,吃一次蘋果,則視爲一次引用,當蘋果全被吃完,則回收對象

package com.lagoon.test;

/**
 * @Author WinkiLee
 * @Date 2019/5/9 22:30
 * @Description 測試對象,以人吃蘋果爲例
 */
public class Apple {

    /**
     * 某個引用被使用的次數
     */
    private int refCount=0;

    /**
     * 引用的個數
     */
    private static long counter=0;

    /**
     * 引用的id
     */
    private final long id=counter++;
    private String name;

    /**
     * 構造器
     */
    public Apple() {
        System.out.println("買了個蘋果\t apple"+id);
    }

    /**
     * 某個引用,每引用一次次數加一
     */
    public void addRef(){
        refCount++;
    }

    /**
     * 銷燬對象
     */
    protected void dispose(){
        if (--refCount==0){
            System.out.println("所有人吃完了蘋果apple"+id+"現在丟掉蘋果核!");
            counter--;
        }else {
            System.out.println("其他人還沒吃完蘋果,蘋果先不銷燬!");
        }
    }

    /**
     * get方法
     */
    public int getRefCount() {
        return refCount;
    }

    public static long getCounter() {
        return counter;
    }

    public long getId() {
        return id;
    }
}
package com.lagoon.test;

/**
 * @Author WinkiLee
 * @Date 2019/5/9 22:41
 * @Description 引用對象,人物
 */
public class Person {

    private Apple apple;
    /**
     * 引用的個數
     */
    private static long counter=0;

    /**
     * 引用的id
     */
    private final long id=counter++;
    private String name;

    /**
     * 構造器
     */
    public Person(Apple apple,String name){
        this.apple=apple;
        this.name=name;
        System.out.println(name+"走進了小屋!");
        this.apple.addRef();
    }

    /**
     * 銷燬對象
     */
    protected void dispose(){
        System.out.println(name+"走出了小屋!");
        apple.dispose();
    }

    /**
     * 吃蘋果
     */
    protected void eatApple(){
        System.out.println(name+"吃了一口蘋果!");
    }

    /**
     * get方法
     */
    public static long getCounter() {
        return counter;
    }

    public long getId() {
        return id;
    }
}
package com.lagoon.test;

/**
 * @Author WinkiLee
 * @Date 2019/5/9 22:47
 * @Description 主函數
 */
public class Main {

    public static void main(String[] args) {

        /**
         * 創建兩個蘋果對象
         */
        Apple apple0=new Apple();
        Apple apple1=new Apple();
        System.out.println("**********************************************************");
        System.out.println("現在的蘋果數量爲:"+Apple.getCounter());
        System.out.println("蘋果apple0被引用的數量爲:"+apple0.getRefCount());
        System.out.println("蘋果apple1被引用的數量爲:"+apple1.getRefCount());
        System.out.println("**********************************************************");

        /**
         * 創建吃蘋果的人
         */
        Person[] persons={ new Person(apple0,"張三"), new Person(apple0,"李四"),
                new Person(apple0,"王五"), new Person(apple0,"趙六"), new Person(apple0,"劉齊") };

        System.out.println("**********************************************************");
        System.out.println("現在的蘋果數量爲:"+Apple.getCounter());
        System.out.println("蘋果apple0被引用的數量爲:"+apple0.getRefCount());
        System.out.println("蘋果apple1被引用的數量爲:"+apple1.getRefCount());
        System.out.println("**********************************************************");

        for(Person p:persons){
            /**
             * 吃完蘋果的人,要從小屋中出去(銷燬對象釋放內存)
             */
            p.eatApple();
            p.dispose();
        }

        System.out.println("**********************************************************");
        System.out.println("現在的蘋果數量爲:"+Apple.getCounter());
        System.out.println("蘋果apple0被引用的數量爲:"+apple0.getRefCount());
        System.out.println("蘋果apple1被引用的數量爲:"+apple1.getRefCount());
        System.out.println("**********************************************************");
    }
}

運行結果:

買了個蘋果	 apple0
買了個蘋果	 apple1
**********************************************************
現在的蘋果數量爲:2
蘋果apple0被引用的數量爲:0
蘋果apple1被引用的數量爲:0
**********************************************************
張三走進了小屋!
李四走進了小屋!
王五走進了小屋!
趙六走進了小屋!
劉齊走進了小屋!
**********************************************************
現在的蘋果數量爲:2
蘋果apple0被引用的數量爲:5
蘋果apple1被引用的數量爲:0
**********************************************************
張三吃了一口蘋果!
張三走出了小屋!
其他人還沒吃完蘋果,蘋果先不銷燬!
李四吃了一口蘋果!
李四走出了小屋!
其他人還沒吃完蘋果,蘋果先不銷燬!
王五吃了一口蘋果!
王五走出了小屋!
其他人還沒吃完蘋果,蘋果先不銷燬!
趙六吃了一口蘋果!
趙六走出了小屋!
其他人還沒吃完蘋果,蘋果先不銷燬!
劉齊吃了一口蘋果!
劉齊走出了小屋!
所有人吃完了蘋果apple0現在丟掉蘋果核!
**********************************************************
現在的蘋果數量爲:1
蘋果apple0被引用的數量爲:0
蘋果apple1被引用的數量爲:0
**********************************************************

Process finished with exit code 0

  • 根搜索算法

    在主流的商用語言中,都是使用根搜索算法判定對象是否存活的,這個算法的基本思路就是通過一系列的名爲“GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連(就是說從GC Roots到這個對象不可達)時,則證明此對象是不可用的。

    例如

在圖中,對象object5,object6,object7雖然互相有關聯,但是它們到GC Roots是不可達的,所以它們會被判定爲是可回收的對象

在java語言中,可作爲GC Roots的對象包括以下幾種

1.虛擬機棧(棧幀中的本地變量表)中的引用的對象

2.方法區中的類靜態屬性引用的對象

3.方法區中的常量引用的對象

4.在本地方法中JNI(即一般說的Native方法)的引用的對象

對象的去留

在根搜索算法中不可達的對象,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:如果對象在進行根搜索後發現沒有與GC Roots相連接的引用鏈,那麼它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視爲“沒有必要執行”。

如果這個對象被判定有必要執行finalize()方法,那麼這個對象將會被放置在一個名爲F-Queue的隊列之中,並在稍後由一條虛擬機自動建立的,低優先級的Finalizer線程去執行。這裏所謂的執行是指虛擬機會觸發這個方法,但是並不承諾會等它運行結束。這樣做的原因是,如果一個對象在finalize()方法中執行緩慢,或者發生了死循環等更極端的情況,將很可能導致F-Queue隊列中的其他對象永遠處於等待狀態,甚至導致整個內存回收系統崩潰。finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模的標記,如果對象要在finalize()中成功拯救自己:只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那麼在第二次標記時它將被移出“即將回收”的集合;如果對象這個時候還沒有逃脫,就會死亡而被回收

一次對象自我拯救的演示

demo:

package com.lagoon.test;

/**
 * @Author WinkiLee
 * @Date 2019/5/10 15:08
 * @Description 1.對象可以在被GC時自我拯救
 * 2.這種自救的機會只有一次因爲一個對象的finalize()方法最多隻會被系統自動調用一次
 */
public class FinalizeEscapeGC {

    public static FinalizeEscapeGC SAVE_HOOK=null;

    public void isAlive(){
        System.out.println("我還活着!");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize 方法被執行了!");
        //建立關聯,擺脫死亡
        FinalizeEscapeGC.SAVE_HOOK=this;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK=new FinalizeEscapeGC();

        //對象第一次嘗試拯救自己
        SAVE_HOOK=null;
        System.gc();

        //因爲Finalizer方法優先級很低,暫停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK!=null){
            SAVE_HOOK.isAlive();
        }else {
            System.out.println("我死了!");
        }

        //對象第二次嘗試拯救自己
        SAVE_HOOK=null;
        System.gc();

        //因爲Finalizer方法優先級很低,暫停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK!=null){
            SAVE_HOOK.isAlive();
        }else {
            System.out.println("我死了!");
        }
    }
}

finalize()方法是怎麼被調用的?在哪裏調用的?

示例一:

package com.jjyy.basic;
/**
 * finalize方法會在什麼時間執行?
 */
public class FinalizeDemo {
	public static void main(String[] args) {
		Demo demo = new Demo();
		System.out.println("begin to set demo to null");
		demo = null;
		System.out.println("demo was set to null");
	}
}
 
class Demo{
 
	@Override
	protected void finalize() throws Throwable {
		System.out.println("Demo finalized");
		super.finalize();
	}
}
/*
結果爲:
begin to set demo to null
demo was set to null
注意:finalize()不一定會在將引用設置爲null的時候
*/

從示例一的結果來看,並沒有在將引用置爲null的時候調用了finalize()方法,所以結論爲:

finalize()方法根本沒有被執行,看一下java中對finalize方法的定義:Called by the garbage collector on an object when garbage collection determines that there are no more references to the object.。

當垃圾回收確認沒有指向對象的引用時,執行回收。而上面的代碼新建的對象Demo的唯一引用d已經被釋放,而確有執行Demo類的finalize方法,唯一的原因只能是gc並沒有執行,gc只有在JVM內存不足的時候纔會自動執行。

示例二:


package com.jjyy.basic;
/**
 * 程序員手動的控制gc()的運行時機
 */
public class FinalizedGCDemo {
	public static void main(String[] args) {
		DemoGC demoGC = new DemoGC();
		System.out.println("begin to set demoGC to null");
		demoGC = null;
		System.out.println("demoGC was set null");
		System.out.println("begin to run gc");
		System.gc();
		System.out.println("gc was runed ");
	}
}
 
class DemoGC{
 
	@Override
	protected void finalize() throws Throwable {
		System.out.println("Demo finalized");
		super.finalize();
	}
}
/*
結果爲:
begin to set demoGC to null
demoGC was set null
begin to run gc
gc was runed 
Demo finalized
*/

結論:
所以finalize方法只有在JVM執行gc時纔會被執行,所以我們在寫代碼用到的時候需注意。

形象點來說就是即將死亡而被回收的對象在gc方法執行時通過執行了finalize()方法,而在這個方法中,這個對象和引用鏈上的存活的對象搭上了關係,於是有了後臺,可以幫助自己脫離死刑,從而避免死亡而被回收

回到自救的代碼片段,代碼中有兩段完全一樣的代碼片段,執行結果卻是一次逃脫成功,一次失敗,這是因爲任何一個對象的finalize()方法都只會被系統自動調用一次,如果對象面臨下一次回收,它的finalize()方法不會再被執行,因此第二段代碼的自救行動失敗

不鼓勵使用finalize()方法來拯救對象,因爲它的運行代價高昂,不確定性大,無法保證各個對象的調用順序。finalize()能做的工作,使用try-finally或其他方式都可以做得更好,更及時。


對方法區的回收

普遍認爲方法區(或者HotSpot虛擬機中的永久代)是沒有垃圾收集的,java虛擬機規範中確實說過可以不要求虛擬機在方法區中實現垃圾收集,而且在方法區進行垃圾收集的“性價比”一般比較低:在堆中,尤其是在新生代中,常規應用進行一次垃圾收集可以回收70%-95%的空間,而永久代的垃圾收集效率遠低於此

永久代的垃圾收集主要回收兩部分內容廢棄常量和無用的類。回收廢棄常量與回收java堆中的對象非常類似。

以常量池中字面量的回收爲例,加入一個字符串“abc” 已經進入了常量池中,但是當前系統沒有任何一個String對象時叫做“abc”的,換句話說就是沒有任何String對象引用常量池中的“abc”常量,也沒有其他地方引用了這個字面量,如果在這時候發生內存回收,而且必要的話,這個“abc”常量就會被系統“請”出常量池。常量池中的其他類(接口),方法,字段的符號引用也與此類似

判斷一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”則相對苛刻,必須同時滿足下列三個條件才能算是“無用的類”

1.該類所有的實例都已經被回收,也就是java堆中不存在該類的任何實例

2.加載該類的ClassLoder已經被回收

3.該類對應的java.Lang.Class對象沒有在任何地方唄引用,無法在任何地方通過反射訪問該類的方法

虛擬機可以對滿足上述三個條件的無用的類進行回收,這裏說的僅僅是可以,而不是和對象一樣,不使用了就必然會回收。是否對類進行回收,HotSpot虛擬機提供了-Xnoclassgc參數進行控制,還可以使用-verbose:class及-XX:+TraceClassLoading,-XX:+TraceClassUnLoading查看類的加載和卸載信息。

-verbose:class和-XX:+TraceClassLoading可以再product版的虛擬機中使用,但是-XX:+TraceClassUnLoading參數需要fastdebug版本的虛擬機支持

在大量使用反射,動態代理,CGLib等bytecode框架的場景,以及動態生成JSP和OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章