偏僻又热门,引用与引用队列

飞天小牛肉

共 5797字,需浏览 12分钟

 ·

2022-03-09 10:28

前文介绍了两种判断对象是否可回收的方法,无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断一个对象是否可达,都和 “引用” 离不开关系。

那么我们是不是真正了解 “引用” 这个东西了?

文章有些图片来源在这里 Java Reference Objects - https://www.kdgregory.com/index.php?page=java.refobj,很 nice,知识体系非常完整,推荐大伙花点时间看下,句子不难,就是专有名词太多,直接谷歌翻译的话会非常生硬,有些知识储备直接读原文会很轻松。

老规矩,背诵版在文末。点击阅读原文可以直达我收录整理的各大厂面试真题

引用和引用指向的对象

A reference object(引用,我感觉这个不如直接写成 reference 更容易理解) is a layer of indirection between your program code and some other object, called a referent(引用指向的对象). Each reference object is constructed around its referent, and the referent cannot be changed.

翻译起来比较简单,引用(reference object)是一个间接层,我们的代码通过引用访问引用指向的对象(referent)

d1260f184ccce76af596cd63173fe936.webp

relationships between application code, soft/weak reference, and referent

所有的引用类型,都是抽象类 java.lang.ref.Reference 的子类:

4156cda17bdbe9e3ddc8cae068086e49.webp

这个抽象类提供了 get 方法用来获取引用指向的对象(referent):

1f637dc4f17f6df37bb79fe02e11c855.webp

举个例子:

SoftReference> ref = new SoftReference>(new LinkedList());

// somewhere else in your code, you create a Foo that you want to add to the list
List list = ref.get();
if (list != null) {
    list.add(foo);
else {
    // list is gone; do whatever is appropriate
}

四种引用的定义

我想大部分人都可以很轻松的说出引用的定义:如果栈中的变量存储的数值代表的是另外一块内存的起始地址,就称该变量代表了这块内存 or 这个对象的引用。

在 JDK 1.2 之前,没有问题,这个定义很正确。

不过现在来看有些过于狭隘了。

举个例子,我们希望引用能够描述这样一类对象:当内存空间还足够时,就保留在内存之中,如果垃圾收集后内存空间比较紧张,那就抛弃这些对象释放空间。

对于上述的定义来说,一个对象只有 “被引用” 和 “未被引用” 两种状态,对这种情况显然是无能无力的。

所以,JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为以下四种,这 4 种引用的强度依次逐渐减弱,所谓 “强度”,可以这样简单理解,引用的强度越强,那么这个被引用的对象就越不容易被垃圾回收器回收掉:

1)强引用,Strongly Re-ference

强引用随处可见,就是最传统的 “引用” 的定义,通过 new 进行的引用赋值,即类似 User user = new User() 这种引用关系。

只要还有强引用指向一个对象,就能表明对象还 “活着”,垃圾收集器永远不会碰这种对象。

换句话说,当内存空间不足的时候,JVM 宁可抛出 OOM,使程序异常终止,也不会回收具有强引用的对象。

2)软引用,Soft Reference

软引用就对应我们上面举的那个例子,可以让对象豁免一些垃圾收集,用来描述一些还有用、但非必须的对象

如果内存空间足够,那么软引用就不会被回收掉,但是如果快要发生 OOM 了,那么 JVM 就会对这些软引用进行回收释放空间,如果对这些软引用回收完了之后还是没有足够的内存,才会抛出 OOM。

在 JDK 1.2 之后提供了 SoftReference 类来实现软引用

3)弱引用,Weak Reference

弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些。

如果你创建了一个仅持有弱引用的对象,那么下一次垃圾收集发生的时候,无论当前内存是否足够,这个对象都会被回收掉。

换句话说,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。

在 JDK 1.2 之后提供了 WeakReference 类来实现弱引用

4)虚引用,Phantom Reference

虚引用也称为 幽灵引用幻影引用幻象引用,它是最弱的一种引用关系。

如果一个对象仅持有幻像引用,那么它就和没有任何引用一样,对其生存时间没有任何影响,我们也无法通过幻像引用来取得一个对象实例(看下图,它的 get 方法永远返回 null)

c35a53bb629f845c65460b2b78ee3f3c.webp虚引用的 get 方法

滑稽了,那幻像引用有啥用?

事实上,我们可以通过为一个对象设置幻像引用关联从而跟踪这个对象被垃圾回收的活动(详细见下文解释)

在 JDK 1.2 之后提供了 PhantomReference 类来实现幻像引用

对象的生命周期

在 JDK1.2 之前,一个对象的生命周期(object life cycle)可以简单的用下图表示:

22b58d391829f78c509b08bc164b029e.webp

object life-cycle, without reference objects

而在 JDK1.2 中,引入了 java.lang.ref 包,一个对象的生命周期中新增了三个状态(stage)

17323eecc7b9708eebf1556f7ec11549.webp

可以看到,除了强引用对应的强可达状态(strongly reachable)之外,额外添加了个三个状态,分别对应软引用、弱引用和虚引用(幻像引用):

  • softly reachable,软可达:就是当我们只能通过软引用才能访问到对象的状态
  • weakly reachable,弱可达:就是无法通过强引用或者软引用访问,只能通过弱引用访问时的状态。
  • phantom reachable,幻象可达:上面流程图已经很直观了,就是没有强、软、弱引用关联,并且被回收掉了,只有幻像引用指向这个对象的时候。

除了幻像引用(因为 get 永远返回 null),如果对象还没有被销毁,都可以通过 get 方法获取原有对象。这意味着,利用软引用和弱引用,我们可以将访问到的对象,重新指向强引用,也就是人为的改变了对象的可达性状态!这也是为什么上面图里有些地方画了双向箭头。

引用队列

引用队列 ReferenceQueue 是用来配合引用工作的,最常与幻像引用一起使用,因为幻像引用的构造函数必须指定引用队列,而其他引用类型没有引用队列一样可以运行。

d900ec06189eafc50d27f649ba4b97a5.webp

当某个被引用的对象(referent)被回收的时候,JVM 会将指向它的引用(reference)加入到引用队列的队列末尾,这相当于是一种通知机制。这个操作其实是由 ReferenceHandler 守护线程来做的,这个守护线程是在 Reference 静态代码块中建立并且运行的线程,所以只要 Reference 这个父类被初始化,该线程就会创建和运行,它的运行方法中依赖了比较多的本地 (native) 方法:

288d526d9c19ac87b5d02f1916100197.webp

由于 ReferenceHandler 是守护线程,除非 JVM 进程终结,否则它会一直在后台运行(注意它的 run() 方法里面使用了死循环)。

64a4d1c8e5f5da86ea4872e9f01703bd.webp

实际上就是调用了引用队列的 enqueue 方法来执行入队操作:

cddd78dcc696b60f5de085102876d4c9.webp

这样,我们可以通过 ReferenceQueue 中的元素(引用)来知道哪些对象(被引用的对象)被回收掉了,通过这种方式,我们就可以在对象被回收掉之后,做一些我们自己想做的事情。

这也就是为什么说幻像引用存在的唯一作用就是跟踪对象被垃圾回收的活动

另外,ReferenceQueue 提供了三种方法来弹出队头元素:

  • poll():用于移除并返回该队列中的下一个引用对象,如果队列为空,则返回null
  • remove():用于移除并返回该队列中的下一个引用对象,该方法会在队列返回可用引用对象之前一直阻塞
  • remove (long timeout):用于移除并返回队列中的下一个引用对象。该方法会在队列返回可用引用对象之前一直阻塞,或者在超出指定超时后结束。如果超出指定超时,则返回null。如果指定超时为0,意味着将无限期地等待。

不同引用类型的应用场景

软引用的应用:断路器

断路器,Circuit Breaker

A better use of soft references is to provide a "circuit breaker" for memory allocation: put a soft reference between your code and the memory it allocates, and you avoid the dreaded OutOfMemoryError.

举个例子,下面这段 JDBC 代码,逻辑是查询数据库的多行数据

e9301a48e3a1dade8a4f533fa61dc1e2.webp

往比较极端的情况想,如果查询到的数据有一百万行,但你的系统的可用内存资源已经不足以装得下这一百万行数据,此时程序肯定就抛错误了。

这个时候软引用的价值就体现出来了:如果在查询数据期间 JVM 已经耗尽了内存,那么被软引用指向的对象的内存就会被释放掉从而给新的数据挪出空间,同时在业务线程上我们可以抛出自定义异常以便我们进行程序的后续处理:

31d2836fdd7a14daeb947a37e2a7c677.webp

弱引用的应用:ThreadLocal 的 ThreadLocalMap 实现

大名鼎鼎,这个本文就不多说了,后续会开文章详细解释。

94f7a6623ae6fa97faa163a413b8a4a2.webp

虚引用的应用:数据库连接池

数据库连接池 Connection Pool 应该具备的一个优点就是能够有效的避免连接资源泄露,同时能够对连接资源进行回收:

下面这个类可以不用怎么看,不过有一点值得注意,用户使用该连接池时业务线程拿到的连接对象正是这个PooledConnection 对象,而不是真正的 Connection 对象

1e6a5f6a1a5fc8492e2bbbdc3c6a4bdd.webp

重点看下下面这个类的实现:

c048388e80a7a4bb025388ca63eaf992.webp

如果引用队列中能够拿到引用,说明连接对象被 GC 掉了,此时我们就应该对连接池执行相应的清理逻辑(重点注意下面的 releaseConnection 方法):

5b4227aadfb62166b11ce92fe94e5d82.webp

看起来挺复杂,其实本质上就是围绕着虚引用的特性:你不能通过它访问对象,但是它结合引用队列提供了一种对象被回收以后做某些事情的机制。


最后放上这道题的背诵版:

🥸 面试官:讲一讲四种引用

😎 小牛肉:JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为以下四种,这 4 种引用的强度依次逐渐减弱,所谓 “强度”,就是引用的强度越强,那么这个被引用的对象就越不容易被垃圾回收器回收掉:

1)强引用,Strongly Re-ference

强引用随处可见,就是最传统的 “引用” 的定义,通过 new 进行的引用赋值,即类似 User user = new User() 这种引用关系。

只要还有强引用指向一个对象,就能表明对象还 “活着”,垃圾收集器永远不会碰这种对象。

2)软引用,Soft Reference

软引用就是,如果内存空间足够,那么软引用就不会被回收掉,但是如果快要发生 OOM 了,那么 JVM 就会对这些软引用进行回收释放空间,如果对这些软引用回收完了之后还是没有足够的内存,才会抛出 OOM。

在 JDK 1.2 之后提供了 SoftReference 类来实现软引用

软引用的经典应用就是断路器,举个例子,如果我们查询到的数据库数据有一百万行,但系统的可用内存资源已经不足以装得下这一百万行数据,此时程序肯定就抛错误了。

这个时候软引用的价值就体现出来了:如果在查询数据期间 JVM 已经耗尽了内存,那么被软引用指向的对象的内存就会被释放掉从而给新的数据挪出空间,同时在业务线程上我们可以抛出自定义异常以便我们进行程序的后续处理

3)弱引用,Weak Reference

弱引用就是,你创建了一个仅持有弱引用的对象,那么下一次垃圾收集发生的时候,无论当前内存是否足够,这个对象都会被回收掉。

换句话说,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。

在 JDK 1.2 之后提供了 WeakReference 类来实现弱引用

弱引用的经典应用就是 ThreadLocal 的 ThreadLocalMap 实现,balabala......

4)幻象引用,Phantom Reference

幻象引用也称为虚引用,它是最弱的一种引用关系。

如果一个对象仅持有幻像引用,那么它就和没有任何引用一样,对其生存时间没有任何影响,我们也无法通过幻像引用来取得一个对象实例(因为它的 get 方法永远返回 null)。所以构造幻像引用的时候必须指定引用队列 ReferenceQueue,不然啥用也没有。

所谓引用队列就是,当某个被引用的对象被回收的时候,JVM 会将指向它的引用加入到引用队列的队列末尾。这个操作其实是由 ReferenceHandler 守护线程来做的,这个守护线程是在 Reference 静态代码块中建立并且运行的线程,所以只要 Reference 这个父类被初始化,该线程就会创建和运行,而具体的引用入队操作其实就是调用了 ReferenceQueue 的 enquque 方法。这样,我们就可以通过 ReferenceQueue 中的元素(引用)来知道哪些对象(被引用的对象)被回收掉了,我们就可以在对象被回收掉之后,来做一些我们自己想做的事情。

幻像引用的经典应用就是数据库连接池,数据库连接池应该具备的一个优点就是能够有效的避免连接资源泄露,同时能够对连接资源进行回收,我们可以用幻像引用来声明数据库的连接对象,这样,如果引用队列中能够拿到引用,就说明对应的连接对象被 GC 掉了,此时就应该对连接池执行相应的清理逻辑,防止我们忘记释放资源导致后续发生资源泄露。



心之所向,素履以往,我是小牛肉,小伙伴们下篇文章再见 👋


浏览 48
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报