您的位置 首页 php

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

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

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

文章有些图片来源在这里 Java Reference Objects – 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)

fbd802f8eee4470bb10e86a2704ad860

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

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

0fdc394df9874feabf5d6c854723491b

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

b41047f39aa74a39911cd9cd55667e5b

举个例子:

 SoftReference<List<Foo>> ref = new SoftReference<List<Foo>>(new LinkedList<Foo>());// somewhere else in your code, you create a Foo that you want to add to the listList<Foo> 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)

9d9c5cf653de444b940107ac9af663c2

虚引用的 get 方法

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

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

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

对象的生命周期

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

1b10a3dd50ad4dacacb87854fc1c7d0a

object life-cycle, without reference objects

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

b47231edd755455e977bee209491c6c1

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

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

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

引用队列

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

2f4d8a949eb449fa94f7e8e7042fc7ab

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

87a3fd5d43f44b068d0cadcad67da02c

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

d99e8d6fd8f3476d998cf7b2acc70d3c

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

dabec6de2407495d9230773578e82bf7

这样,我们可以通过 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 OutOfMemory Error .

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

01c4eb4a3c4b499494842b5c2cae619a

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

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

8c2c692cba3e46cbbce2eb314ef60df9

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

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

876d1f8aeb6346538edac67568dc598e

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

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

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

93e6335bc0d247d69bc787284367c33d

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

92f3660a87d5436aa9ebfe314058bb42

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

4c5b848915c143ec844e521f6f25c179

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


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

面试官 :讲一讲四种引用

小牛肉 :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 掉了,此时就应该对连接池执行相应的清理逻辑,防止我们忘记释放资源导致后续发生资源泄露。

来源:

文章来源:智云一二三科技

文章标题:偏僻又热门,引用与引用队列

文章地址:https://www.zhihuclub.com/47353.shtml

关于作者: 智云科技

热门文章

网站地图