• 垃圾回收相关算法


    这里介绍的垃圾回收相关算法,主要解决的问题:

    判断哪些内存是垃圾(需要回收的)?

    常用的两种算法:

    • 引用计数
    • 可达性分析(GC Root)

    首先介绍算法前,得定义:

    如何判断一个对象的死亡?

    我们一般这样定义:当一个对象不再被任何存活的对象继续引用的时候,这个对象就死亡了。

    引用计数

    引用计数算法,是给每一个对象添加一个计数器,当有对象引用它的时候,计数器+1,当有对象取消对它的引用时,计数就会-1。

    当计数器的值为 0 时,即说明没有对象引用它,也就是这个对象死亡了。

    这种算法很简单,但是有个重大缺陷,那就是无法解决循环引用的问题。

    什么是循环引用问题呢?

    比如对象A 引用 对象B,对象B 引用 对象A,那么 对象A 和 对象B 的计数器都为1。但是如果后续的运行环境再也用不到对象A 和 对象B,那么就造成了内存泄漏。

    上图就是循环引用的例子。对象引用 Obj1 和 Obj2 在栈中,然后分别指向在堆中的具体实例。然后两个相互实例中的成员互相引用。那么对于堆中的对象而言,就有2个引用。一个是来自Obj1,一个来自堆对象的另一方。

    如果,现在将 Obj1 指向 nu l l,那么就如下图:

    这个时候,引用已经不可用了,但是堆中的对象仍然相互引用,他们的计数器不为0,所以无法死亡。

    但是,Java 没有使用这种算法,而是使用了我们后面说的可达性算法,所以接下来的演示,GC 会将这种情况的内存给其清理。

    package GC;
    
    public class ReferenceCountGC {
        public Object instance = null;
    
        private static final int _1MB = 1024 * 1024;
        // 每个对象中包含2M的成员,方便观察
        private byte[] bigSize = new byte[2 * _1MB];
        public static void main(String[] args) {
            ReferenceCountGC objA = new ReferenceCountGC();
            ReferenceCountGC objB = new ReferenceCountGC();
            objA.instance = objB.instance;
            objB.instance = objA.instance;
    
            //取消对对象的引用
            objA = null;
            objB = null;
          // 是否进行垃圾回收
            System.gc();
        }
    }
    

    这段代码实现的就是上面图片所描述的情况。

    首先,我们将 System.gc() 注释掉,也就是我们在默认情况下,不去触发垃圾回收。并在运行的时候,添加参数 -XX:+PrintGCDetails。我们观察输出结果

    可以看到,这个时候,占用的空间为8M左右。

    如果我们取消注释,也就是主动去调用垃圾回收器,那么运行结果为:

    占用空间为2M左右。

    可以看出来,Java 的垃圾回收,并非采用我们上面介绍的引用计数方式。

    可达性分析

    可达性算法,还有一系列的别名:根搜索算法,追踪性垃圾收集,GC Root。

    之后,看到原理,其实这些别名都是描述原理的。

    首先,我们选取一些对象,这些对象是存活的,也被称为 GC Roots,然后根据这些对象的引用关系,凡是直接或者间接跟 GC Roots 相关联的对象,都是存活的。就像图中的连通性判断一样。

    这个算法的想法不难。难的是,如何确定 GC Roots。

    我们考虑,我们什么时候需要用到对象?(我们需要对象的时候,肯定需要这个对象是存活的)

    • 栈中保存着,我们当前或者之后需要运行的方法及相关参数,所以,栈上所引用的堆中对象肯定是存活的。
    • 类中的一些属性,比如,静态属性,因为它不依赖于具体的类
    • 一些常用的对象,以免清理后,又要重复加载,比如常用的异常对象,基本数据类型对应的 Class 对象。

    除此之外,还有很多零零碎碎的。

    在堆结构周围的一些结构,其中引用的对象可以作为GC Roots

    具体 GC Roots 可以概括为:

    • 虚拟机栈上(确切的说,是栈帧上的本地变量表)所引用的对象

    • 本地方法栈引用的对象

    • 方法区中的静态属性,常量引用

    • Java 虚拟机的内部引用,常用数据类型的 Class 对象,常驻的异常对象,系统类加载器

    • 所有被同步锁持有的对象

    除此之外,还有一些临时的 GC Roots 可以加入进来。这里涉及到新生代老年代。

    比如老年代中的对象一般都存活时间比较久,也就是大概率是活着的对象,也可临时作为 GC Roots。

    可达性算法的一些细节

    前面说了可达性算法,我们根据 GC Roots 来进行标记对象的死活。

    但是,被判定为不可达的对象,并不立刻死亡。它仍然有次机会进行自救。

    这个自救的机会,是需要重写 finalize()进行自救。

    也就是可达性算法的逻辑大致是这样的:

    • 第一次进行标记,凡是不可达 GC Roots 的对象,都暂时判定为死亡,只是暂时
    • 检查暂时被判定为死亡对象,检查是否有重写 finalize()方法,如果有,则触发,对象可以在里面完成自救。

    如果没有自救成功 或者 没有重写 finalize()方法,则宣告这个对象的死亡。

    除此之外,这个对象中的 finalize()方法,只能被调用一次,一生只有一次自救机会。

    这个方法,官方并不推荐,所以不必细究。

    接下来,演示下上面的两次标记过程以及自救过程。

    (个人认为,《深入理解 Java 虚拟机》中的此章节代码,略有点不够完善,故略微改动)

    package GC;
    
    import javax.swing.tree.VariableHeightLayoutCache;
    
    public class FinalizeEscapeGC {
        public static FinalizeEscapeGC SAVE_HOOK = null;
    
        private byte[] bigSize = new byte[5*1024*1024];
    
        public void isAlive(){
            System.out.println("Yes, i am 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();
                System.gc();
            }else {
                System.out.println("Dead");
                System.gc();
            }
        }
    }
    

    在这个程序中,我们给这个类,添加名为 bigSize 的属性,其占用 4M 大小的空间。

    大致分析下代码逻辑:

    • 创建了一个对象,其中有成员占用 4M 的空间
    • 取消对这个对象的引用
    • 调用垃圾回收(第一次标记)
    • 调用 finalize 方法进行自救
    • 之后再次调用垃圾回收(第二次标记)

    所以演示的时候,分为两种情况:

    • FinalizeEscapeGC.SAVE_HOOK = this; 未注释,完成自救

    运行时,参数仍然设置为 +XX:PrintGCDetails,可以看到输出结果:

    第一次调用垃圾回收,仍然占用 5M,说明此时即便失去引用,但是仍然未被清理。

    在 finalize()中完成自救后,第二次调用垃圾回收的时候,仍然占用 5M 的内存大小。说明自救成功。

    • FinalizeEscapeGC.SAVE_HOOK = this; 注释,无法完成自救

    第一次垃圾回收,占用 5M,保留了对象。无法完成自救,然后第二次被清理掉。

    所以我发现以下表述也许更为确切:

    • 当对象重写了 finalize()方法的时候,第一次垃圾回收的时候,如果为不可达对象,对其进行暂缓,并不清理。
    • 当对象没有重写 finalize()方法的时候,且为不可达对象的时候,直接判定死亡。
  • 相关阅读:
    BASE64
    2020-2021-1 20201217《信息安全专业导论》第二周学习总结
    师生关系20201217
    2020-2021-1 20201217王菁<<信息安全专业导论>>第一周学习总结
    作业正文(快速浏览教材提出问题)
    自我介绍20201217王菁
    罗马数字转阿拉伯数字(20201225张晓平)
    20201225 张晓平《信息安全专业导论》第三周学习总结
    20201225 张晓平《信息安全专业导论》第二周学习总结
    我期待的师生关系
  • 原文地址:https://www.cnblogs.com/zhouzhiyao/p/13171764.html
Copyright © 2020-2023  润新知