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

引用计数算法

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

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

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

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

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

原理图就是如下

第一步:创建A对象,存储在堆空间中,但是a变量是存储在栈帧里面的局部变量表中,所以a的引用地址就是堆空间引用地址
第二步:创建B对象,存储在堆空间中,但是b变量也是存储在栈帧里面的局部变量表中,所以b的引用地址就是堆空间引用地址
第三步:A对象的属性object的引用地址指向了B对象的引用地址
第四步:B对象的属性object的引用地址也执行了A对象的引用地址
代码图中的第五步:局部变量表中的a变量引用地址置为null,直接将下图中的第一步去掉了
代码图中的第六步:局部变量表中的b变量引用地址置为null,直接将下图中的第二步去掉了
这样就导致了堆空间中的循环相互引用的问题
jvm垃圾回收之引用计数算法和可达性分析算法(判断对象是否存活算法_第2张图片
可达性分析算法(有称之:根搜索算法)

可达性分析算法的基本思路就是通过一系列名为”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。

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

jvm垃圾回收之引用计数算法和可达性分析算法(判断对象是否存活算法_第3张图片
jvm垃圾回收之引用计数算法和可达性分析算法(判断对象是否存活算法_第4张图片

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

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

以下是参考:深入理解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队列处于等待,严重会导致内存崩溃,这是我们不希望看到的。

你可能感兴趣的:(算法,java,jvm,编程语言,python)