引用计数法和可达性算法

引用计数法和可达性算法

困而学,学而知

好记性不如烂笔头

紧接着上一篇JVM老生常谈之运行时数据区,我们已经连接了Java虚拟机几个运行时数据区,今天我们接着来讲讲Java虚拟机的几个重要的内存回收算法。本文所涉及的知识基本都基于HotSpot虚拟机。

首先,我们先来认识两个普遍用于判断对象是否被引用的算法:引用计数法可达性算法

引用计数法其实很简单,如果对象的计数器为0,就说明对象不再被引用,否则就是再被引用。

可达性算法则是通过判断对象是否能够被GC ROOT访问到来判断对象是否还在被引用。

引用计数法

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

但是引用计数法其实是很难解决对象之间相互循环引用的问题,所以,Java虚拟机里面没有选用引用计数算法来管理内存。

我们用下面的例子来验证一下上面说的是否正确。

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;
      
      //假设在这行发生gc,objA和ObjB是否能被回收
      System.gc();
    }
}

GC输出

可以看到,其实也是有被回收了,也就是意味着虚拟机并没有因为两个对象相互引用就不回收他们。侧面说明虚拟机并不是通过引用计数法来判断对象是否存活。

虽然可引用计数法很简单,也经常被提及,但是HotSpot虚拟机却不是用这个算法来判断对象是否继续被引用,而是使用下面要介绍的算法:可达性分析算法。

可达性分析算法

在主流商用程序语言的主流实现中,都是称通过可达性分析来判定对象是否存活的。

算法的基本思路就是通过一系列称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径被称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用的。

可达性分析算法示例

上图中,Object1~Object4都可以被GC Root访问到,而Object5~Object7都不可以被访问到,这也就是说。也就是说,Object5、6、7这三个对象就是不可达的,下次垃圾回收的时候,可能就会被回收掉。

其实并不是所有的对象都可以作为GC Roots的对象,只有下列的对象可以作为GC Roots的对象。

可以作为GC Roots的对象

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

既然是引用计数法,那肯定就有各种引用,下面来说说一些引用。

引用的两次标记过程

上面介绍了这么多,其实还有一个点没有讲到,我们要宣告一个对象死亡,至少要经历两次标记过程:

  1. 第一次标记: 如果对象在进行可达性分析后发现没有 GCRoots 相连接的引用链,那么它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。如果对象被判定为有必要执行,则会被放到一个F-Queue队列。

  2. 第二次标记:finalize()方法是对象跳脱死亡命运的最后一次机会,稍后GC将对F-Queue中对象进行第二次小规模标记,如果对象要在finalize()中重新拯救自己:只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时她将被移出即将回收的集合。

强引用、软引用、弱引用和虚引用

JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种。

JDK1.2之前,只有被引用和没有被引用两种状态

  • 强引用:指在程序代码之中普遍存在的,类似Object obj = new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收被引用的对象
  • 软引用:用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用
  • 弱引用:用来描述非必需对象,但是他的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用
  • 虚引用:也被称为幽灵引用或者幻影引用。它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。供对象被finalize之后,执行指定的逻辑的机制(cleaner)

简单总结

简单的对上面做一个总结,在JVM中判断一个对象是都需要回收有两种算法:引用计数法和可达性算法。引用计数法是通过判断引用的计数器的值是否为0来确认回收与否。这种算法听起来很简单,但是存在一个缺陷,就可以可能存在循环引用的情况。

还有一种就是可达性算法,可达性算法是通过判断引用能够被 GC Roots 访问到来确认回收与否。能被称为GC Roots对象也是有条件的主要有四种:虚拟机栈中引用的对象、方法中类静态属性引用的对象、方法中常量引用的对象和本地方法栈(native方法)中JNI引用的对象。

引用分为四种类型:强引用、软引用、弱引用和虚引用。

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://baozi.fun/archives/count-reachable

Buy me a cup of coffee ☕.