读书笔记之三:垃圾回收算法前奏 -- 判断对象是否存活
一、引用的定义
JDK 1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但太过狭隘,一个对象在这种定义下只有被引用和没有被引用两种状态,对于一些“食之无味,弃之可惜”的对象显得无能为力。
为描述这样一类对象:当内存空间足够时,则能保留在内存中;如果内存空间在进行垃圾回收后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。
JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,引用强度依次逐渐减弱。
1.1 强引用
强引用就是指在程序代码中普遍存在的,类似“Object obj = new Object()” 这类的引用,垃圾收集器永远不会回收该引用类型的对象。
1.2 软引用
软引用用来描述一些还有用但是非必需的对象。这类引用对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在 JDK 1.2 之后,提供了 SoftReference 类来实现软引用。
1.3 弱引用
弱引用也是用来描述非必需对象的,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在 JDK 1.2 之后,提供了 WeakReference 类来实现弱引用。
1.4 虚引用
虚引用也称为幽灵引用或者幻影引用,是最弱的一种引用关系。一个对象是否存在虚引用,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在 JDK 1.2 之后,提供了 PhantomReference 类来实现虚引用。
二、引用计数算法
引用计数法的基本思路:给对象添加一个引用计数器,每当有一个地方引用它,计数器值就加 1 ;当引用失效时,计数器值就减 1;任何时刻计数器为 0 的对象就是不可能再被使用的。
但是,主流的 Java 虚拟机里面没有选用引用计数法来管理内存,其中主要的原因是它很难解决对象之间相互循环引用的问题。
三、可达性分析算法
3.1 可达性分析算法基本思路
可达性分析算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,相当于:图论中,从 GC Roots 到这个对象不可达时;则证明此对象是不可用的。
3.2 可作为 GC Roots 的对象
其中,Java 语言中,可作为 GC Roots 的对象包括下面几种:
- 虚拟机栈(栈帧中本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中 JNI (即一般说的 Native 方法)引用的对象。
3.3 无用对象的自我拯救
当一个对象被判断为无用对象时,不会被 GC 立即回收,而要至少经历两次标记过程:如果对象通过可达性分析后没有与 GC Roots 相连接的引用链,会被第一次标记并进行第二次筛选;筛选条件是该对象是否有必要执行 finalize() 方法,若该对象没有重写该方法或已经调用过一次,都会被判定为没有必要执行。如果有必要执行,会被放置在 F-Queue 队列中,稍候由虚拟机自动建立一个低优先级的线程执行,在这个过程中,只要对象被重新引用,就不会被回收了。
需要注意的是,此处的虚拟机创建线程执行,只是虚拟机会触发这个方法,当并不会承诺等待它运行结束。原因是,如果一个对象在 finalize() 方法中执行缓慢,或者发生了死循环,将可能导致 F-Queue 队列中其他对象永久处于等待,甚至导致整个内存回收系统的崩溃。
3.4 GC停顿
3.4.1 GC 停顿 -- 设置安全点
执行引用可达法时,需要 GC 停顿。即能确保一致性的快照(一致性就是指在整个分析时间期间整个执行过程看起来被冻结在某个时间点上)。可设置安全点,采用抢先式中断或者主动式中断。
抢先式中断不需要线程的执行代码主动去配合,在 GC 发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应 GC 事件。
主动式中断的思想就是当 GC 需要中断线程时,不直接对线程操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起,轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
设置安全点的问题:设置安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入 GC 的安全点。但是,程序不执行的时候,也就是程序没有分配到 CPU 时间,典型的例子就是线程处于 Sleep 状态或者 Blocked 状态,这时候线程无法响应 JVM 的中断请求,JVM 也不可能等待线程重新被分配 CPU 时间。对于这种问题,就需要安全区域(Safe Region)来解决。
3.4.2 GC 停顿 -- 设置安全区域
安全区域是指在一端代码中,引用关系不会发生变化。在这个区域中的任意地方开始 GC 都是安全的。可以将其看做扩展了的安全点。在线程执行到 Safe Region 中的代码时,首先标识自己已经进入了 Safe Region。这样,当在这段时间内 JVM 要发起 GC 时,就不用管标识自己为 Safe Region状态的线程了 。当线程要离开 Safe Region 时,检查系统是否已经完成了根节点枚举(或者整个 GC 过程),如果完成了,那线程就继续执行,否则必须等待直到收到可以安全离开 Safe Region 的信号为止。