Java四种引用类型

文章目录

  • 引用类型简介
  • 对象可达性判断
    • GC Roots对象
    • 可达性判断
      • 可达性状态类型
      • 可达性判断规则
      • 状态转换
  • Reference
    • 类定义
    • 状态变化
    • 四种引用类型实现
      • 强引用FinalReference
      • 软引用SoftReference
      • 弱引用WeakReference
      • 虚引用PhantomReference

引用类型简介

Java开发者肯定都很熟悉java中的4种引用类型,它们从强到弱分别是:

引用类型 对象是否可引用 回收时间 使用场景
强引用 可以 从不回收 普遍对象的状态
软引用 可以 内存不足时 内存敏感的高速缓存
弱引用 可以 下一次GC 对象缓存
虚引用 不可以 下一次GC,不影响对象生命周期 必须和引用队列(ReferenceQueue)一起使用,一般用于追踪垃圾收集器的回收动作。相比对象的finalize方法,虚引用的方式更加灵活和安全。

但可能大多数对4种引用类型的理解都只停留在概念的记忆上,而并不了解四种引用的实现原理。下面我们结合源代码了解下四种引用的实现原理。当然在了解四种引用的实现原理之前,首先得了解下JVM中实现对象可达性判断的原理。

对象可达性判断

目前,大多数JVM都是使用可达性分析算法来判断对象的是否可达。可达性分析算法以GC Roots对象作为起始点进行搜索。当一个对象与GC Roots对象没有任何引用链相连时(也即引用有向图中从GC Roots对象到这个对象是不连通的),则表明该对象是不可用的(不可用的对象不一定被判定为可以回收的对象)。当对象与GC Roots对象有引用链相连时,则需要根据引用链的类型来判断对象是否可达。

不可用的对象不一定被判定为可以回收的对象:判定对象为”死亡”至少需要经历两次标记的过程。第一次标记:对象可达性分析,如果发现对象没有与GC Roots相连接的引用链,且对象需要执行finalize方法,将会被加入F-Queue队列中。第二次标记:由一个优先级低的Finalizer线程去取F-Queue队列的对象,“尝试执行”对象的finalize方法。

JVM会保证触发满足条件的对象的finalize方法,但是并不承诺会等待方法执行结束。finalize方法是对象逃脱死亡命运的最后一次机会。

GC Roots对象

GC Roots对象包含以下四类:

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

可达性判断

可达性状态类型

Java有5种类型的可达性状态:

  • 强可达(Strongly Reachable):与GC Roots对象之间有强引用相连通,则为强可达的;
  • 软可达(Soft Reachable):与GC Roots对象之间没有强引用相连通,但有软引用相连通,则为软可达的;
  • 弱可达(Weak Reachable):与GC Roots对象之间没有强引用或软引用相连通,但有弱引用相连通,则为弱可达的;
  • 虚可达(Phantom Reachable):与GC Roots对象之间没有强引用、软引用或弱引用相连通,然后该对象finalize方法已执行,并且有虚引用相连通,则为虚可达的;
  • 不可达(Unreachable):如果对象finalize方法已执行并且没有任何引用相连通,则对象是不可达的,可以被回收。

可达性判断规则

从GC Roots对象到一个对象的引用链可能存在多条,那么此时会依据两个原则来判断对象的可达性:

  • 单个引用链中,以最弱的引用类型为准;
  • 多引用链联合看时,以最强的引用类型为准;

Java四种引用类型_第1张图片

首先,单个引用链中,以最弱的引用类型为准:则GC Roots->Obj1->Obj4是软引用连通的,GC Roots->Obj2->Obj4是弱引用连通的,GC Roots->Obj3->Obj5是弱引用连通的。然后多引用链联合看时,以最强的引用类型为准:则GC Roots到Obj4对象的引用联合来看是弱引用连通的。

状态转换

对象可达性状态是随着程序运行而不断变化的,对象可达性状态转换图可参考下图。

  • 对象创建后一般是强可达的。
  • 当GC Roots对象到该对象的强引用被清除后:如果剩余引用链最高为软引用,则状态转换为软可达的;反之如果最高为弱引用,则状态转换为弱可达的,反之则把对象标记为可执行finalize方法状态。
  • 当软可达对象重新被强引用连接时,则转换为强可达状态;当软可达对象的软引用被清除后,如果剩余引用链最高为弱引用,则状态转换为弱可达;反之则把对象标记为可执行finalize方法状态。
  • 当弱可达对象重新被强引用或者软引用连接时,则可转换为强可达或者软可达;当弱可达对象的弱引用被清除后,则把对象标记为可执行finalize方法状态。
  • 可对象被标记为可执行finalize方法状态,如果对象finalize从未被执行,则执行finalize方法,并标记对象的finalize方法已经被执行(在finalize方法可能会重新生成强/软/弱引用等,对象状态会重新转换为强/软/弱可达,不过并不推荐这么做,因为可能会导致对象状态紊乱,无法被正常回收);反之当对象有虚引用连接时,则转换为虚可达状态,否则转换为不可达状态。
  • 虚可达对象在垃圾回收后状态转换为不可达(不能通过虚引用获取对象引用,所以对象状态不会再转换为强/软/弱可达);

Java四种引用类型_第2张图片

Reference

Reference类是所有引用类型的基类,定义了reference对象的通用操作,用来保存对象引用及引用的内部状态。Reference抽象类初始化时,会启动一个ReferenceHandler线程。Reference的referent被回收前,垃圾回收器会把reference添加到pending这个链表里(如果注册了ReferenceQueue),然后ReferenceHandler线程不断的读取pending中的reference,把它加入到对应的ReferenceQueue中(如果Reference是Cleaner类的实例,即虚引用对象,则调用其注册的预处理钩子方法)。

ReferenceQueue提供了两个静态字段NULL,ENQUEUED。这两个字段的主要功能:NULL是当我们构造Reference实例时queue传入null时,会默认使用NULL,这样在enqueue时判断queue是否为NULL,如果为NULL直接返回,入队失败。ENQUEUED的作用是防止重复入队,reference后会把其queue字段赋值为ENQUEUED,当再次入队时会直接返回失败。

类定义

Reference对象的基类。该类定义了reference对象的通用操作。因为reference对象是和垃圾回收器密切配合实现的,因此该类不能直接进行子类化。

public abstract class Reference {
	// 用于保存对象的引用,GC会特别对待该变量
    private T referent; 
	// 如果注册了ReferenceQueue(需要通知机制),用来保存对象引用的队列,
    volatile ReferenceQueue queue;

    // 保存需要由ReferenceHandler处理的引用
    volatile Reference next;

	// 被JVM使用,保存需要被JVM处理的下一个引用
    transient private Reference discovered;

    // 同步锁,用于同步pending队列的进队和出队
    static private class Lock { }
    private static Lock lock = new Lock();

    // 一个PENDING队列,配合上述next一起使用,实现类单向循环链表的操作
    private static Reference pending = null;

	// 高优先级线程,用于将pending队列里面的Reference实例依次添加到不同的ReferenceQueue中
	private static class ReferenceHandler extends Thread {...}
	...
}
 
  

状态变化

一个对象引用有四种内部状态:

  • Active: 新创建的实例的状态,当对象引用被垃圾回收器回收前:如果Reference注册了ReferenceQueue,则会切换为Pending,并且Reference会加入pending链表中,如果没有注册ReferenceQueue,会切换为Inactive。
  • Pending: 在pending链表中的Reference的状态,这些Reference等待被ReferenceHandler内部线程加入ReferenceQueue中。
  • Enqueued: 在ReferenceQueue队列中的Reference的状态,如果Reference从队列中移除,会进入Inactive状态。未注册ReferenceQueue的实例不会到达该状态。
  • Inactive: Reference的最终状态,该状态不会再改变。

其状态转换图如下:

Java四种引用类型_第3张图片

四种引用类型实现

不同引用类型的UML类图如下:

Java四种引用类型_第4张图片

强引用FinalReference

对象新建后默认为强引用类型的,是普遍对象引用的类型。查看FinalReference在JDK中的源码发现其只有一个空实现,这也说明强引用是“默认引用类型”。

/**
 * Final references, used to implement finalization
 */
class FinalReference extends Reference {
    public FinalReference(T referent, ReferenceQueue q) {
        super(referent, q);
    }
}

软引用SoftReference

软引用是用来描述一些“还有用但是非必须”的对象。软引用的回收策略在不同的JVM实现会略有不同,JVM不仅仅只会考虑当前内存情况,还会考虑软引用所指向的referent最近的使用情况和创建时间来综合决定是否回收该referent。软引用保存了两个变量:

  • timestamp:每次调用get方法都会更新时间戳。JVM可以利用该字段来选择要清除的软引用,但不是必须要这样做。
  • clock:时间锁,由垃圾收集器更新。

因此,任何GC都可以使用这些字段并定义清除软引用的策略,例如:最后清除最近创建的或最近使用的软引用。

/**
 * 软引用对象由垃圾收集器根据内存需要决定是否清除。软引用经常用于实现内存敏感的缓存。
 *
 * 假如垃圾收集器在某个时间确定对象是软可达的,此时它可以选择原子地清除
 * 指向该对象的所有软引用,以及从该对象通过强引用链连接的其他软可达对象的所有软引用。
 * 与时同时或者之后的某个时间,它会将注册了reference queues的新清除的软引用加入队列。
 *
 * 在虚拟机抛出OutOfMemoryError异常之前,将保证清除对软可达对象的所有软引用。
 * 不过,并没有对清除软引用的时间以及清除顺序施加强制约束。
 * 但是,鼓励虚拟机实现偏向不清除最近创建或最近使用的软引用。
 *
 * 该类的直接实例可用于实现简单的缓存。
 * 该类或其派生子类也可用于更大的数据结构以实现更复杂的高速缓存。
 * 只要软引用的引用对象还是强可达的,即还在实际使用中,软引用就不会被清除。
 * 因此,复杂的高速缓存可以通过持有对最近使用缓存对象的强引用来防止其被清除,
 * 而不常使用的剩余缓存对象由垃圾收集器决定是否清除。
 */
public class SoftReference extends Reference {

	// 时间锁,由垃圾收集器更新。
    static private long clock;

	// 每次调用get方法都会更新该时间戳。JVM可能会在选择要清除的软引用时使用该字段,
	// 但这不是强制必须的。
    private long timestamp;
	
	// 返回对象的引用。如果该引用对象已经被程序或者垃圾收集器清除,则返回null。
	// 把最近一次垃圾回收时间赋值给timestamp
    public T get() {
        T o = super.get();
        if (o != null && this.timestamp != clock)
            this.timestamp = clock;
        return o;
    }

}

弱引用WeakReference

当一个对象没有被强引用或者软引用连接,但被弱引用连接时,则处于弱可达状态。只要发生GC,弱可达的对象就会被清除,同时会把弱引用加入到注册的引用队列中(如果存在的话)。弱引用对GC几乎是没有影响的,它不影响对应的referent被终结(finalized)和回收(reclaimed)。因此,弱引用最常用于实现规范化映射(canonicalizing mappings),例如哈希表,如果它们在程序中未被引用,则其键和值将从映射中删除。

/**
 * 弱引用对象不能阻止自身的引用被回收。
 * 弱引用常用于实现规范化映射(对象实例可以在程序的多个地方同时使用)。
 *
 * 假如垃圾收集器在某个时间点确定对象是弱可达的。那时它将原子地清除对该对象的所有弱引用
 * 以及该引用通过强引用或者软引用连接的所有其他弱可达对象的所有弱引用。
 * 同时,它将表明前面所指的所有弱可达对象都可以执行finalize方法。
 * 与此同时或之后某一个时间,它将注册了reference queues的那些新清除弱引用加入队列。
 */
public class WeakReference extends Reference {

	// 创建没有注册ReferenceQueue的弱引用
    public WeakReference(T referent) {
        super(referent);
    }

	// 创建注册了ReferenceQueue的弱引用
    public WeakReference(T referent, ReferenceQueue q) {
        super(referent, q);
    }
}

虚引用PhantomReference

虚引用是所有引用类型中最弱的一种。一个对象是否关联到虚引用,完全不会影响该对象的生命周期,也无法通过虚引用来获取一个对象的实例。为对象设置一个虚引用的唯一目的是:能在此对象被垃圾收集器回收的时候收到一个系统通知,它就是利用ReferenceQueue实现的。当referent被gc回收时,JVM自动把虚引用对象本身加入到ReferenceQueue中,表明该reference指向的referent被回收。然后可以通过去queue中取到reference,可以通过这个来做额外的清理工作。可以用虚引用代替对象finalize方法来实现资源释放,这样更加灵活和安全。

/**
 * 虚引用对象在被垃圾收集器检查到后加入reference queues队列,否则会被回收。
 * 虚引用最常用于实现比Java finalization机制更灵活的安排额外的清理工作。
 *
 * 如果垃圾收集器在某个时间点确定虚引用对象是虚可达的,那么在那个时间或之后某个时间它会将引用加入reference queues队列。
 *
 * 为了确保可回收对象保持不变,虚引用的引用无法使用:虚引用对象的get方法始终返回null。
 *
 * 与软引用和弱引用不同,当虚引用加入reference queues队列后垃圾收集器不会被自动清除。
 * 只通过虚引用可达的对象将保持不变,直到所有此类引用都被清除或自已变为不可达。
 */
public class PhantomReference extends Reference {

    // 由于不能通过虚引用访问对象,因此此方法始终返回null。
    public T get() {
        return null;
    }

    // 使用空ReferenceQueue队列创建一个虚引用没有意义:它的get方法总是返回null,
	// 并且由于它没有注册队列,所以也不会被加入队列有任何清理前的预处理操作。
    public PhantomReference(T referent, ReferenceQueue q) {
        super(referent, q);
    }
}

你可能感兴趣的:(java)