Java虚拟机--之--垃圾回收机制篇

1. 概述

学习Java到一定阶段,对于Java虚拟机的学习是不可缺少的部分,了解C/C++的开发者都知道,内存由开发者主动申请,同时对象不用了还需要手动去释放内存,否则很容易造成内存泄漏。然而Java开发者则无需这方面的关注,只有少数部分例如文件流、数据库操作等需要做相应的关闭操作即可,内存的申请和释放都由Java虚拟机来为我们完成。上一篇文章Java虚拟机--之--内存模型篇 从虚拟机发展历程的角度分析了内存模型,本篇文章将基于此,从对象是否可回收、回收算法、垃圾收集器三个方面来分析JVM的垃圾回收机制,大致内容如下:

2. 对象回收算法

要实现垃圾回收,首先需要考虑的就是如何去判断哪些垃圾(内存)需要回收,经过多年的发展,现在已经有很好的算法来判断哪些对象处于存活状态,哪些对象已经死去。

2.1 引用计数算法

  • 概念

引用计数算法就是给对象添加一个引用计数器,每当有一个地方引用该对象的时候就会+1,相反当失去一个引用的时候就-1,当引用数为0的时候也就说明这个对象不在被使用就可以被回收。

这种算法实现简单,判定效率也很高,在大部分情况下它是一个不错的算法,但是唯一的缺点就是在两个对象相互引用的时候那么他们的引用就不会为0,那么GC也就无法回收他们,因此当前主流的Java虚拟机基本上没有选用引用计数算法来管理内存。下面举一个例子来说明,两个对象相互引用,GC却不能回收他们的原因。

public class ReferenceCountingGC {

    public Object instance = null;

    public static void main(String []args){
        // 第一部分 
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        // 第二部分
        objA.instance = objB;
        objB.instance = objA;
        // 第三部分
        objA = null;
        objB = null;

    }
}

如果使用引用计数算法来管理内存,判断内存是否可以回收,那么上面的例子即便为objA和objB制空,也是无法回收的,这是因为:

  1. 第一部分代码,开辟两块内存,对应的实例我们暂且命名为A和B,分别被objA和objB引用,因此A、B两个对象的引用计数器分别为1;
  2. 第二部分代码,由于又有各自的instance指向内存A和B,因此A、B两个对象的引用计数器分别为2;
  3. 第三部分代码,由于给objA和objB制空,因此A、B两个对象的引用计数器分别减1,最后A、B两个对象的引用计数器还是为1,不为0,因此不能回收。

2.2 可达性分析算法

  • 概念

这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连是,则表明该对象不可达,则可以进行内存回收。如下图,Object5、6、7是可以被回收的。

 

那么问题来了,哪些对象可以作为“GC Roots”的对象呢?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象;

其实对于可达性分析算法,并不是发生一次GC就会对不在GC Roots相连接的引用链的对象进行回收,有些对象可能处于“死缓”状态,对于这些对象至少要经历两次标记过程。如果对象在进行可达性分析后发现没有在引用链上,它将会被第一次标记并进行筛选,筛选的条件是此对象是否有必要执行finalize方法,有必要执行finalize方法的对象就属于“死缓”状态的对象。

扫描二维码关注公众号,回复: 2986925 查看本文章

           那么如何判断是否有必要执行finalize方法呢?

当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过一次,这两种情况只要满足其一,都表示没必要执行finalize()方法。

对于被判定有必要执行finalize()方法的对象,GC会将该对象放到一个叫F-Queue的队列之中,并由虚拟机自动创建的一个Finalizer线程去执行各个对象的finalize()方法,在该过程即会进行第二次标记过程,如果某些对象存在“自我救赎”现象,则会将这些对象移出“即将回收”的集合,那对于没有移出的对象,基本上就真正回收了。

           什么样的情况属于对象的“自我救赎”?

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()....");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String []args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();
        SAVE_HOOK = null;   // 第一次制空
        System.gc();

        Thread.sleep(1000);

        if(SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no, i am dead ...");
        }

        SAVE_HOOK = null;  // 第二次制空

        System.gc();

        Thread.sleep(1000);

        if(SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no, i am dead ...");
        }
    }

}

// 输出如下:

finalize()....
yes, i am still alive ...
no, i am dead ...

通过上述代码,可以看到,第一次制空时,执行了finalize方法,由于发生了对象的引用,造成该对象不能进行回收,这样就发生了对象的自我救赎,SAVE_HOOK并不为null,当第二次制空过后,由于不会再执行finalize方法,因此该对象接下来将会被回收。

  • 总结

对于如何判定对象是否能够被回收,主要就是以上两种算法,并且现在大多数主流JVM都是基于第二种算法实现。

2. 垃圾收集算法

垃圾收集算法有很多种,并且涉及到大量的程序细节,因此本篇文章仅仅理论上来分析现在主流的算法实现细节。

2.1 标记-清除算法

标记-清除(Mark-Sweep)算法可以算是最基础的垃圾收集算法,该算法主要分为“标记”和“清除”两个阶段。先标记可以被清除的对象,然后统一回收被标记要清除的对象,这个标记过程采用的就是可达性分析算法。

标记清除算法最大的缺点是在垃圾回收之后会产生大量的内存碎片,而如果内存碎片多了,当我们再创建一个占用内存比较大的对象时就没有足够的内存来分配,那么这个时候虚拟机就还要再次触发GC来清理内存后来给新的对象分配内存。

具体执行过程如下图所示:

2.2 复制算法

为了解决效率问题及标记清除算法的缺点,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块用完了,触发GC操作将存活的对象复制到另一个区域当中,然后再把使用过的内存空间一次清理掉。这样使得每次都对整个半区进行内存回收,内存分配也不用考虑内存碎片的问题,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

这种算法最大的缺点是将原有内存分成了两块,每次只能使用区中一块,也就是损失了50%的内存空间,代价有点大。 

具体执行过程如下图所示:

2.2 标记-整理算法

复制算法在对象存活率较高的情况下需要进行较多的复制操作,这样效率也会变低,更关键的是还需要浪费50%的内存空间,为了解决这些问题,于是“标记-整理”(Mark-Compact)算法就出来了,标记过程仍然使用可达性算法来判断,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理边界以外的内存。

 

 

未完待续、、、

 

 

 

参考文献

https://blog.csdn.net/yulong0809/article/details/77421615

 

 

 

 

 

猜你喜欢

转载自blog.csdn.net/u010349644/article/details/82191822