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的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出

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