深入了解JDK中的Reference

前言

Java在1.2版本之前只有普通的强引用,只要对象存在引用,则对象就不会被回收,即使内存不足,也是如此,JVM抛出了OOME时,也不会去回收存在引用的对象。

如果只提供强引用,那我我们很难写出这个对象不是很重要,如果内存不足时,可以GC回收掉这样的语义代码来。所幸的是,Java在1.2版本中完善了引体系,提供了四种引用类型: 强引用软引用弱引用虚引用我们不但可以控制垃圾回收器对对象的回收策略,同时还能在对象被回收后得到通知,进行相应的后续操作

引用与可达性分类

Java目前有四种引用类型:

  • 强引用(Strong Reference)
    普通的引用类型,new一个对象默认得到的就是强引用类型,只要对象存在强引用,就不会被GC掉。
  • 软引用(Soft Reference)
    垃圾回收器会在内存不足时回收软引用指向的对象。JVM会在抛出OOME前清理所有软引用指向的对象,如果清理完还是内存不足,才会抛出OOME。所以软件引用一般用于实现内存敏感(memory-sensitive)的缓存设计。
  • 弱引用(Weak Reference)
    弱引用对象,它不阻止将其引用变成finalizablefinalizedreclaimed。换一种说法,即垃圾回收器在GC时会回收些对象。
  • 虚引用(Phantom Reference)
    虚引用是一种比较特殊的引用类型,不能通过虚引用获取到关联对象,只是用于获取对象被回收的通知。

使用可达性分析来判断一个对象是否存活,其基本的思路是从GC Root开始向下搜索,如果对象与GC Root之间存在引用链,则对象是可达的。对象的可达性与引用类型密切相关。

Java中有五种类型的可达性:

  • 强可达(Strongly Reachable)
  • 软可达(Soft Reachable)
  • 弱可达(Weak Reachable)
  • 虚可达(Phantom Reachable)
  • 不可达(Unreachable)

对象的引用类型与可达性的对应关系,我们使用下面的例子来讲一下:
有5个对名,obj1~obj5, 每个对象只有一个引用,分别为:
obj1 - 强引用 ----> obj1 - 强可达;
obj2 - 软引用 ----> obj2 - 软可达;
obj3 - 弱引用 ----> obj3 - 弱可达;
obj4 - 虚引用 ----> obj4 - 虚可达;
obj5 - 无引用 ----> obj5 - 不可达 (obj5没有存在和GC Root的引用链,所以不可达);

Reference结构图

java.lang.ref包中类关系图

Reference的核心

Java中的多种引用类型的实现,不是通过扩展语法实现的,而是利用类实现的,Reference类表示一个引用,其核心代码就是一个成员变更referent,其他部分代码如下:

public abstract class Reference<T> {
  /**
  * 一个引用的对象可能会有以下四种状态中的一种:
  * Active: 新创建的对象实例状态即为Active状态。这个实例如果注册为队列,则进入Pending状态,否则进入Inactive状态。
  * Pending: 未注册的实例不会到达这个状态;在pendingReference列表中的元素,等待Reference-handler线程enqueue操作
  * Enqueued: 未注册的实例不会到达这个状态;当实例从ReferenceQueue列表中删除时,进入Inactive状态。
  * Inactive: 一旦实例变为Inactive(非活动)状态,它的状态将不再更改。
  */
  private T referent;  /*被GC特别的处理*/
 /**
  * 返回引用管理的对象,如果这个对象已经被回收,则返回null.
  */
  public T get() {
    return this.referent;
  }
}

我们从上文提到了,Reference类及其子类有两大功能:

  • 实现了特定的引用类型
    对于这个功能点是怎么实现的呢?
    如果JVM没有对referent这个变量做特殊处理,它依然只是一个普通的强引用,之所以会出现不同的引用类型,是因为JVM垃圾回收器硬编码识别SoftReferenceWeakReferencePhantomReference等这些具体的类,对其reference变量进行特殊对象处理,才有了不同的引用类型的效果。
  • 用户可以在对象被回收后得到通知
    看到这个问题,我们想到的首先想到的解决方案就是:在新建一个Reference实例时,添加一个回调,当java.lang.ref.Reference#referent被回收时,JVM调用该回调。这种思路就是我们一般的通知模型,但是对于引用与垃圾回收这种底层场景来说,会导致实现复杂,性能不高的问题,比如需要考虑在什么线程中执行这个回调,回调执行阻塞怎么办等。
    而通过Reference的源码,我们了解到它使用了一种更加原始的方式来做通知,就是把引用对象被回收的Reference添加到一个队列中,用户后续自己从队列中获取并使用。

Reference相关部分代码如下:

public abstract class Reference<T> {
  // ...

  // referent被回收后,当前Reference实例会被添加到这个队列中
  volatile ReferenceQueue<? super T> queue;

  // ...

  // 只传入referent的构造函数,意味着用户只需要特殊的引用类型,不关心对象何时被GC
  Reference(T referent) {
      this(referent, null);
  }
  // 传入referent和ReferenceQueue的构造函数,referent被回收后,会添加到queue中
  Reference(T referent, ReferenceQueue<? super T> queue) {
      this.referent = referent;
      this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
  } 

  // ...
}

Reference的状态

Reference对象是有状态的,一共有四种:

  • Active - 新创建的实例状态,由垃圾回收器进行处理,如果实例的可达性(reachability)处于合适的状态,垃圾回收器会切换实例的状态为Pending或Inactive。如果Reference注册了ReferenceQueue,则切换为Pending,并且Reference会加入到pending-Reference链表中,如果没有注册ReferenceQueue,会切换为Inactive。
  • Pending - 在pending-Reference链表中的Reference的状态,这些Reference等待被加入到ReferenceQueue中。
  • Enqueued - 在ReferenceQueue队列中的Reference的状态,如果Reference从列表中移除,会进入Inactive状态。
  • Inactive - Reference的最终状态,不可改变。

其状态图转换如下:


Reference状态转换图

在Reference的四种状态中,我们看到有一个pending-Reference链表,我们接下来探讨一下这个链表是用来干什么的。

在reference引用的对象被回收后,该Reference实例会被添加到ReferenceQueue中,但是这个不是垃圾回收器来做的,这个操作还是一定的复杂度的,如果垃圾回收器还要执行这个操作,就会降低其效率。
所以垃圾回收器做的是一个非常轻量级的操作:把Reference添加到pending-Reference链表中。Reference对象中有一个静态的pending成员变量,它就是这个pending-Reference链表的头结点。而另一个成员变量discovered就是这个链表的指针,指向下一个节点。
相应的代码段如下:

public abstract class Reference<T> {
  // ...

 /**
  * Active: 由垃圾回收器管理的已发现的引用列表
  * pending: 在pending列表中的下一个元素,如果没有为null
  */
  transient private Reference<T> discovered;  /* used by VM */
  // 全局唯一的pending-Reference列表
  private static Reference<Object> pending = null;

  // ...
}

ReferenceHandler线程

在上面,我们已经知道一个Referenece实例化后的状态为Active,当其引用的对象被回收之后,垃圾回收器将其加入到pending-Reference链表中,等待加入 ReferenceQueue。那这个过程是由谁来实现的呢?
这个过程不能对垃圾回收器产生影响,所以不能在垃圾回收线程中执行,也就需要一个独立的线程来负责,这个线程就是ReferenceHandler,它的定义在Reference类中:

public abstract class Reference<T> {
  // ...

 /**
  * 用于控制垃圾回收器操作与Pending状态的Reference入队操作不冲突执行的全局锁
  * 垃圾回收器开始一轮垃圾回收前要获取此锁
  * 所以所有占用这个锁的代码必须尽快完成,不能生成新对象,也不能调用用户代码
  */
  static private class Lock { }
  private static Lock lock = new Lock();
  // 最高优先级的线程
  private static class ReferenceHandler extends Thread {

    private static void ensureClassInitialized(Class<?> clazz) {
      try {
        Class.forName(clazz.getName(), true, clazz.getClassLoader());
      } catch (ClassNotFoundException e) {
        throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
      }
    }

    static {
     // 预加载、初始化InterruptedException和Cleaner类,
     // 这样防止在以后的运行循环中遇到延迟加载、初始化它们时出现内存不足的问题。
      ensureClassInitialized(InterruptedException.class);
      ensureClassInitialized(Cleaner.class);
    }
    ReferenceHandler(ThreadGroup g, String name) {
      super(g, name);
    }
    public void run() {
        // 线程一直运行
        while (true) {
          tryHandlePending(true);
       }
    }
 }

 static boolean tryHandlePending(boolean waitForNotify) {
  Reference<Object> r;
  Cleaner c;
  try {
    synchronized (lock) {
      // 判断pending链表是否有数据
      if (pending != null) {
        r = pending;
        c = r instanceof Cleaner ? (Cleaner) r : null;
        // 从pending链中删除r
        pending = r.discovered;
        r.discovered = null;
      } else {
        // 等待锁可能会引用OutOfMemoryError错误,因为它可以试图分配异常对象
        if (waitForNotify) {
          lock.wait();
        }
        // 重试
        return waitForNotify;
      }
    }
  } catch (OutOfMemoryError x) {
    // 让出线程的CPU时间,这样希望能删除一些活动引用,使用GC回收一些空间
    Thread.yield();
    // 重试
    return true;
  } catch (InterruptedException x) {
    // 重试
    return true
  }
  // cleaner快递通道
  if (c != null) {
    c.clean();
    return true;
  }
  // 把Reference添加到关联的ReferenceQueue中
  // 如果Reference构造时没有关联ReferenceQueue,会关联ReferenceQueue.NULL,这里就不会进行入队操作了
  ReferenceQueue<? super Object> q = r.queue;
  if (q != ReferenceQueue.NULL) q.enqueue(r);
  return true;
 } 

  // ...
}

ReferenceHandler线程是在Reference类中的static块中启动的:

static {
        // 获取系统的线程组
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        // 这里设置了一个最高优先级的线程
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();

        // 在SharedSecrets中提供访问权限
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });
}

从上面的代码中我们知道,ReferenceHandler是一个最高优先级的线程,其逻辑是从Pending-Reference链表中取出Reference,添加到其关联的ReferenceQueue中。

ReferenceQueue

上面我们一直在引用ReferenceQueue,现在我们一起看一下ReferenceQueue类对象:

public class ReferenceQueue<T> {
    // 构造一个新的队列
    public ReferenceQueue() { }

    private static class Null<S> extends ReferenceQueue<S> {
        boolean enqueue(Reference<? extends S> r) {
            return false;
        }
    }

    static ReferenceQueue<Object> NULL = new Null<>();
    static ReferenceQueue<Object> ENQUEUED = new Null<>();
    // 这个锁用于保护链表队列在多线程环境下的正确性
    static private class Lock { };
    private Lock lock = new Lock();
    private volatile Reference<? extends T> head = null;
    private long queueLength = 0;

    boolean enqueue(Reference<? extends T> r) { /* 仅被Reference调用 */
        synchronized (lock) {
            // 判断Reference是否需要入队
            ReferenceQueue<?> queue = r.queue;
            if ((queue == NULL) || (queue == ENQUEUED)) {
                return false;
            }
            assert queue == this;
            r.queue = ENQUEUED;
            r.next = (head == null) ? r : head;
            head = r;
            queueLength++;
            if (r instanceof FinalReference) {
                sun.misc.VM.addFinalRefCount(1);
            }
            lock.notifyAll();
            return true;
        }
    }

    @SuppressWarnings("unchecked")
    private Reference<? extends T> reallyPoll() { /* 必须持有锁才可调用 */
        Reference<? extends T> r = head;
        if (r != null) {
            head = (r.next == r) ?
                null :
                r.next; // Unchecked due to the next field having a raw type in Reference
            r.queue = NULL;
            r.next = r;
            queueLength--;
            if (r instanceof FinalReference) {
                sun.misc.VM.addFinalRefCount(-1);
            }
            return r;
        }
        return null;
    }
 
    public Reference<? extends T> poll() {
        if (head == null)
            return null;
        synchronized (lock) {
            return reallyPoll();
        }
    }

    // 删除些队列中的下一个引用对象,阻塞到有一个可用或者超过给定的时间。
    // 此方法不提供精确的时间保证,因为它是通过调用Object#wait(long)方法来调度超时的。
    public Reference<? extends T> remove(long timeout)
        throws IllegalArgumentException, InterruptedException
    {
        if (timeout < 0) {
            throw new IllegalArgumentException("Negative timeout value");
        }
        synchronized (lock) {
            Reference<? extends T> r = reallyPoll();
            if (r != null) return r;
            long start = (timeout == 0) ? 0 : System.nanoTime();
            for (;;) {
                lock.wait(timeout);
                r = reallyPoll();
                if (r != null) return r;
                if (timeout != 0) {
                    long end = System.nanoTime();
                    timeout -= (end - start) / 1000_000;
                    if (timeout <= 0) return null;
                    start = end;
                }
            }
        }
    }

    // 删除此队列中的下一个引用对象,阻塞直到其中一个可用为止
    public Reference<? extends T> remove() throws InterruptedException {
        return remove(0);
    }

}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • ReferenceQueue 引用队列,在检测到适当的可到达性更改后,垃圾回收器将已注册的引用对象添加到该队列中 ...
    tomas家的小拨浪鼓阅读 35,793评论 10 59
  • 感知GC。怎么感知:* 通过get来判断已经被GC(PhantomReference 在任何时候get都是null...
    YDDMAX_Y阅读 1,769评论 0 4
  • JDK1.2之后,Java扩充了引用的概念,将引用分为强引用、软引用、弱引用和虚引用四种。 强引用类似于”Obje...
    lesline阅读 4,841评论 0 0
  • 深秋北风来 衣单人也困 笑脸把客迎 夜半醉客多 虽遭恶言骂 生作世间人 生活本就难 不必生闷气 忍气吞声莫生事,为...
    映卿阅读 277评论 0 0
  • 生活中最严重的错误莫过于将不可叙述的事物误认为是愚蠢的东西。在某种程度上,这种错误酷似火鸡问题,将我们没有看到的东...
    王增利阅读 376评论 0 0