专栏首页java相关资料jvm垃圾回收之引用计数算法和可达性分析算法(判断对象是否存活算法

jvm垃圾回收之引用计数算法和可达性分析算法(判断对象是否存活算法

引用计数算法

在java中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到,那么这个对象就成为可被回收的对象了。这种方式成为引用计数法。

什么是引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1,引用数量为0的时候,则说明对象没有被任何引用指向,可以认定是”垃圾”对象

这种方法实现比较简单,且效率很高,但是无法解决循环引用的问题,因此在java中没有采用此算法(但是在Python中采用的是此算法)

看下图代码:来分析一下为什么会产生循环引用的问题,且注意看图中的注释

原理图就是如下

第一步:创建A对象,存储在堆空间中,但是a变量是存储在栈帧里面的局部变量表中,所以a的引用地址就是堆空间引用地址
第二步:创建B对象,存储在堆空间中,但是b变量也是存储在栈帧里面的局部变量表中,所以b的引用地址就是堆空间引用地址
第三步:A对象的属性object的引用地址指向了B对象的引用地址
第四步:B对象的属性object的引用地址也执行了A对象的引用地址
代码图中的第五步:局部变量表中的a变量引用地址置为null,直接将下图中的第一步去掉了
代码图中的第六步:局部变量表中的b变量引用地址置为null,直接将下图中的第二步去掉了
这样就导致了堆空间中的循环相互引用的问题

可达性分析算法(有称之:根搜索算法)

可达性分析算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。 这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。

在Java语言中,可作为GCRoots对象包含为以下几种:

虚拟机栈(栈帧中的本地变量表)中引用的对象。(可以理解为:引用栈帧中的本地变量表的所有对象)
方法区中静态属性引用的对象(可以理解为:引用方法区该静态属性的所有对象)
方法区中常量引用的对象(可以理解为:引用方法区中常量的所有对象)
本地方法栈中(Native方法)引用的对象(可以理解为:引用Native方法的所有对象)

可以理解为:

(1)首先第一种是虚拟机栈中的引用的对象,我们在程序中正常创建一个对象,对象会在堆上开辟一块空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种情况是最常见的。
(2)第二种是我们在类中定义了全局的静态的对象,也就是使用了static关键字,由于虚拟机栈是线程私有的,所以这种对象的引用会保存在共有的方法区中,显然将方法区中的静态引用作为GC Roots是必须的。
(3)第三种便是常量引用,就是使用了static final关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也应该作为GC Roots。
(4)最后一种是在使用JNI技术时,有时候单纯的Java代码并不能满足我们的需求,我们可能需要在Java中调用C或C++的代码,因此会使用native方法,JVM内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为GC Roots。

下图:蓝色代表可用对象,红色判定为可回收对象

看下图代码:在了解了可达性分析算法之后,来分析一下为什么Java要使用可达性算法来判断对象是否被回收,且注意看图中的注释

以下是参考:深入理解jvm一书中的:对象的生存还是死亡

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize() 方法。当对象没有覆盖finalize() 方法,或者finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queuc的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是如果一个对象在finalizeO 方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize() 方法是对象逃脱死亡命运的最后一次机公稍后GC将对F-QUCUC中的对象进行第二次小规模的标记,如果对象要在finalize()中成功扬救自己一只要重新与引用链上的任何- 一个对象建立关联即可,譬如把自己(this 关键字) 赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合: 如果对象这时候还没有逃脱,那基本上它就真的被回收了。从代码清单3-2 中我们可以看到一个对象的finalize()被执行,但是它仍然可以存活。代码清单3-2一次对象自我拯救的演示。

package ccc;
/*此代码演示了两点
 * 对象可以在GC时自我拯救
 * 这种自救只会有一次,因为一个对象的finalize方法只会被自动调用一次
 * */
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK=null;
    public void isAlive(){
        System.out.println("yes我还活着");
    }
    public 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();
        //因为finalize方法的优先级很低所以暂停0.5秒等它
        Thread.sleep(500);
        if(SAVE_HOOK!=null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no我死了");
        }
        //下面的代码和上面的一样,但是这次自救却失败了
        //对象的第一次回收
        SAVE_HOOK=null;
        System.gc();
        Thread.sleep(500);
        if(SAVE_HOOK!=null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no我死了");
        }
    }
}

总结:

1.引用计数算法
早期判断对象是否存活大多都是以这种算法,这种算法判断很简单,简单来说就是给对象添加一个引用计数器,每当对象被引用一次就加1,引用失效时就减1。当为0的时候就判断对象不会再被引用。
优点:实现简单效率高,被广泛使用与如python何游戏脚本语言上。
缺点:难以解决循环引用的问题,就是假如两个对象互相引用已经不会再被其它其它引用,导致一直不会为0就无法进行回收。

2.可达性分析算法
目前主流的商用语言[如java、c#]采用的是可达性分析算法判断对象是否存活。这个算法有效解决了循环利用的弊端。
它的基本思路是通过一个称为“GC Roots”的对象为起始点,搜索所经过的路径称为引用链,当一个对象到GC Roots没有任何引用跟它连接则证明对象是不可用的。
要真正宣告对象死亡需经过两个过程。
1.可达性分析后没有发现引用链
2.查看对象是否有finalize方法,如果有重写且在方法内完成自救[比如再建立引用],还是可以抢救一下,注意这边一个类的finalize只执行一次,这就会出现一样的代码第一次自救成功第二次失败的情况。[如果类重写finalize且还没调用过,会将这个对象放到一个叫做F-Queue的序列里,这边finalize不承诺一定会执行,这么做是因为如果里面死循环的话可能会时F-Queue队列处于等待,严重会导致内存崩溃,这是我们不希望看到的。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • JVM垃圾回收之垃圾收集算法,程序员必须掌握的知识

    解释下,堆大小=新生代+老年代,新生代与老年代的比例为1:2,新生代细分为一块较大的Eden空间和两块较小的Survivor空间,分别被命名为from和to。

    黎明大大
  • JVM内存逃逸与栈上分配,程序员必须掌握的知识

    下图中,可以看到直接将User对象返回出去,这样这个User对象有可能会被其他地方所改变,这样他的作用域就不只是在方法内部了,这样就是逃逸到方法外部了

    黎明大大
  • JVM内存分配机制之栈上分配与TLAB的区别

    在java开发中,我们普遍认知中,new出的对象是直接分配到堆空间中,而实际情况并非如此,其实大家伙可以思考一下,无论方法的生命周期长与短,只要new的对象就存...

    黎明大大
  • JVM 垃圾回收机制

    首先JVM的内存结构包括五大区域: 程序计数器、虚拟机栈、本地方法栈、方法区、堆区。其中程序计数器、虚拟机栈和本地方法栈3个区域随线程启动与销毁, 因此这几个区...

    烟草的香味
  • 对象的最后一次救赎

    我们都知道当堆内存不够用的时候,会进行垃圾回收,回收的则是对象,那么哪些对象会被作为”垃圾“被回收呢?

    用户7386338
  • 《深入理解 Java 虚拟机》学习 -- 垃圾回收算法

    给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加 1;当引用失效时,计数器就减 1;任何时刻计数器都为 0 的对象就是不可能再被使用的。

    希希里之海
  • JVM垃圾回收机制和算法详解

    我们今天先聊聊jvm的垃圾回收算法,大家先了解垃圾算法有哪些,在去学习有哪些垃圾回收器,然后我们在学习如何对jvm进行参数调优。

    公众号 IT老哥
  • 编程中的死亡对象

      在之前的 Java内存区域文章中已经知道几乎所有Java对象实例都存放在堆中,GC对堆进行回收之前先是判断哪些对象已经“死亡”。那么问题来了,怎么样确定一个...

    GreizLiao
  • Java对象的结构与对象在内存中的结构

    当我们在Java中使用new这个指令创建一个对象的时候,对象的创建到底经过了什么样的一个过程呢?

    星如月勿忘初心
  • 【JVM从小白学成大佬】4.Java虚拟机何谓垃圾及垃圾回收算法

    在Java中内存是由虚拟机自动管理的,虚拟机在内存中划出一片区域,作为满足程序内存分配请求的空间。内存的创建仍然是由程序猿来显示指定的,但是对象的释放却对程序猿...

    猿人谷

扫码关注云+社区

领取腾讯云代金券