Java引用类型
1. 简介
JVM 的垃圾收集机制是通过可达性分析算法来判断对象是否存活,JVM 在最初设计的版本只会回收那些从 GC Roots 开始无法通过引用链到达的对象,而对于可达的对象 JVM 宁可抛出OutOfMemoryError
也不会回收。但是在实际应用中,对象的重要性不同,重新创建的消耗也不同,而且有很多对象虽然是可达的,但是事实上可能已经不再使用了(比如某个对象集合,只有其中一小部分在使用,其余的基本上都不会被使用了,这些不会被使用的对象,因为是可达的,所以 JVM 不会回收,这浪费了内存空间);在这些情况下,如果对所有对象都严格按照可达性分析来决定是否回收,那么就会影响到内存的使用效率,增加内存溢出的风险,所以 Java 在某个版本后引入了几种特殊的引用类型,这些特殊的引用类型是由抽象基类java.lang.ref.Reference<T>
及其实现子类定义的,它们与 GC 紧密合作,会被 GC 特殊处理,从而提供更为灵活的垃圾回收机制。
可达性分析算法基本思路就是通过一系列被称为 GC Roots 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为__引用链__(Reference Chain),如果某个对象和 GC Roots 间没有任何引用链相连,或者用图论的话来说就是从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的。
##2. 引用强度分类
目前Java中的引用类型按照引用强度依次分为强引用、软引用、弱引用、虚引用四种,不同的引用强度,GC 会采取不同的回收措施,这使不同引用类型的引用对象具有不同的生存周期。我们来看看这几种引用强度的区别。
- 强引用:就是直接引用,Java 中的引用变量一般都是强引用,
Object strongRef = new Object()
就是强引用对象,只要一个对象到_GC Roots_有一条强引用链,就是强引用可达或强可达的,强可达对象不会被 GC 回收。 - 软引用:由(
java.lang.ref.SoftReference<T>
)类定义,SoftReference<Object> softRef = new SoftReference<Object>(new Object())
就是一个软引用对象,如果一个对象到 GC Roots 没有强引用链,但有软引用链(引用链的某一个关键节点是通过软引用实例连接的,如果拿掉该节点则变成不可达),那么该对象就是软引用可达;对于软引用可达对象,如果内存空间足够,GC 不会回收该对象,但是如果内存空间不足了,只要该对象没有变成强可达的,就会被回收。 - 弱引用:由(
java.lang.ref.WeakReference<T>
)类定义,WeakReference<Object> weakRef = new WeakReference<Object>(new Object())
就是一个弱引用对象,如果一个对象到 GC Roots 没有强引用链和软引用链,有弱引用链(引用链的某一个关键节点是通过弱引用实例连接的,如果拿掉该节点则变成不可达),那么该对象就是弱引用可达或弱可达;对于弱可达对象,在某次 GC 时间,GC 线程在扫描内存区域的过程中,发现了弱可达对象,那么在下一次 GC 时间,只要该对象的可达性没有增强(变成软引用可达或强可达),那么弱可达对象会被 GC 回收。 - 虚引用:由(
java.lang.ref.PhantomReference<T>
),PhantomReference<Object> phantomRef = new PhantomReference<Object>(new Object(), queue)
就是一个虚引用对象;如果一个对象到 GC Roots 没有强引用链、软引用链以及弱引用链,只有虚引用链(引用链的某一个关键节点是通过虚引用实例连接的,如果拿掉该节点则变成不可达),那么该对象就是虚引用可达;对于虚引用可达对象,其可达性已经无法改变了(意味着无法增强其可达性),因为无法通过虚引用实例获取到引用对象;所以只要 GC 发现了虚引用可达对象,在下次 GC 时间,该对象一定会被回收。
3. 引用实现分类
上一节我们介绍了Java中四种不同强度的引用类型,这一节我们看看这些引用类型是如何实现的。Java中的引用类型按照实现方式可以分为直接引用类型和特殊引用类型(也是间接引用类型)两种;其中直接引用类型也是强引用类型,而软引用、弱引用、虚引用都由java.lang.ref.Reference<T>
对应的实现子类所定义,都是特殊引用类型 。我们先来看看直接引用方式和特殊引用方式有什么不同。
2.1 两种实现方式的区别
直接引用对象可以直接使用,而特殊引用对象需要通过特殊引用实例才能使用。
图1:两种引用对象的内存模型
- 直接引用:直接引用就是我们在 Java 编程中通常所说的引用,使用引用变量和赋值运算符号
=
来实现,通过声明引用变量,然后使用赋值运算将引用变量和引用对象关联在一起;比如Object directRef = new Object();
,创建了一个直接引用变量,同时构造了一个新的对象,让directRef
变量直接关联到新创建的对象,使用引用对象的时候通过变量directRef
即可; - 特殊引用:特殊引用采用了委托代理模式,通过构造引用实例(就是
java.lang.ref.Reference<T>
及其字类的实例对象)来间接关联目标对象,比如Reference<Object> specRef = new WeakReference<Object>(new Object());
创建了一个弱引用实例specRef
,并且将新创建的对象(new Object()
)关联到引用实例上;使用这个引用对象的时候需要先从引用实例specRef
中通过get
方法获取到引用对象;而之所以称为特殊引用,是因为特殊引用实例及其关联对象,会被 GC 进行特殊处理。
本文中,我们会将直接引用称为引用变量,特殊引用类型
java.lang.ref.Reference<T>
及其子类的实例称为引用实例,被引用变量和引用实例关联的对象称为引用对象。
2.2 特殊引用类型与 JVM GC 的协同机制
最终引用(java.lang.ref.FinalReference
),软引用(java.lang.ref.SoftReference<T>
)、弱引用(java.lang.ref.WeakReference<T>
)、虚引用(java.lang.ref.PhantomReference<T>
)都是 java.lang.ref.Reference<T>
的实现子类,他们共同定义了Java中的特殊引用类型,特殊引用类型的特殊之处在于 JVM GC 会对特殊引用实例进行特殊处理;主要就是和引用实例之间应用一种协同机制对引用实例所关联的对象进行回收,不同的特殊引用类型与 GC 的协同机制也不同。下面我们通过 java.lang.ref.Reference<T>
和 java.lang.ref.ReferenceQueue<Object>
的实现源码来看看这些协同机制。
3. Reference 解析
java.lang.ref.Reference<T>
引用类的主要作用就是同 GC 垃圾收集器协同工作,对其关联的引用对象进行更有效的回收。在 Java 中,我们将类的实例叫做对象,引用实例就是引用类的对象,引用实例与其他类实例的区别在于,因为需要和 GC 协同工作, GC 会对引用实例进行特殊对待。引用实例在 JVM 中有4种状态,特定条件出现时会发生状态转移。
3.1 引用实例在 JVM 中的状态
- Active (活跃):引用对象还未被回收的引用实例是 Active 状态,在 GC 检测到引用实例关联的引用对象的可达性变化到恰当程度时(此时会根据引用对象当前可达性的种类,采取具体的回收机制),引用实例的状态会转化为 Pending 或者 Inactive 状态;如果注册了用户引用队列(就是在创建引用实例时,传入的
ReferenceQueue
实例,该实例由用户自行创建,用于收集被回收了引用对象的引用实例,并由用户程序进行后续处理),那么会将该引用实例添加到 pending-Reference 列表(pending-Reference 列表是一个全局的list
,其链表头为Reference.pending
)里面,从而进入 Pending 状态,如果没有注册用户引用队列,那么该引用实例直接进入 Inactive 状态;
引用对象的可达性变化:前面我们讲了,按照引用强度,有四种引用类型,对于一个对象来说,在 JVM 中可能会被关联到多个不同种类的引用,而该对象的可达性是由其存在的强度最高的那个引用链所决定的,我结合下面的代码和图2,就可看到,对象的可达性会伴随其引用链的变化而变化;而变化到恰当的程度,就是当引用对象的可达性变化到引用实例的协同机制需要的状态。如图2:在T1时刻,对象是强可达,此时 GC 对其采用强可达回收机制,而到了T3时刻,对象的强引用链和软引用链都被打断,此时 GC 就会对该对象实施弱可达回收机制。
/* T1 时刻,对象被4种引用类型关联,引用对象可达状态为强引用可达,JVM按照强可达机制进行回收 */Object strongRef = new Object();SoftReference<Object> softRef = new SoftReference<Object>(strongRef, queue);WeakReference<Object> weakRef = new WeakReference<Object>(strongRef, queue);PhantomReference<Object> phantomRef = new PhantomReference<Object>(strongRef, queue);/* T2 时刻,强引用链被打断,引用对象可达状态变为软引用可达,JVM按照软引用可达机制进行回收*/strongRef = null;/* T3 时刻,软引用链也被打断,引用对象可达状态变为弱引用可达,JVM按照弱可达机制进行回收*/softRef.clear();softRef = null;/* T4 时刻,弱引用链也被打断,引用对象可达状态变为虚引用可达,JVM按照虚引用可达机制进行回收*/weakRef.clear();weakRef = null;
图2:对象的可达性变化
- Pending(挂起) :一旦将引用实例加入 pending-Reference list(挂起引用列表)中时,该引用实例就处于 Pending 状态;该状态的引用实例等待被 Reference-handler 线程加入到用户注册的引用队列中排队等待处理;创建时没有注册用户引用队列的实例不会进入该状态;
- Enqueued(排队):被 Reference-handler 线程加入到用户引用队列的实例会进入排队状态;引用队列中的引用实例被移除后会进入 Inactive 状态;创建时没有注册引用队列的实例也不会进入该状态;
- Inactive(失活):该引用实例生命周期结束,状态不会再发生变化了,等待被 GC 清理。
###3.2 Reference的属性
java.lang.ref.Reference<T>
中的属性如下:
1. private T referent; 2. volatile ReferenceQueue<? super T> queue;3. volatile Reference next;4. transient private Reference<T> discovered; /* used by VM */5. static private class Lock { }private static Lock lock = new Lock();6. private static Reference<Object> pending = null;7. private static class ReferenceHandler extends Thread { ... }
referent
是一个引用变量,用于关联被引用的对象(见图1)。queue
属性是java.lang.ref.ReferenceQueue<T>
的实例,是用户引用队列,需要用户自行创建队列实例,并且通过构造器传给引用实例,用于收集被回收了引用对象的引用实例,ReferenceQueue
与Reference
紧密配合,Reference
就是ReferenceQueue
的节点类型,Reference
中的next
属性就是指向队列ReferenceQueue
中的下一个节点的引用。next
属性是指向queue
中的 下一个节点的指针,如果整个队列只有一个节点,那么next
指向自身;discovered
属性是由 JVM 使用的属性;GC 在扫描内存区域时,会将发现的引用实例维护在一个引用列表中,discovered
就指向了引用列表中的下一个元素。lock
属性,是私有的静态属性,lock
是一个用作锁的对象,用于同步 GC 线程和 ReferenceHandler 线程对挂起引用队列(就是pending
属性)的访问;GC 必须获得lock
锁才能开始回收。pending
是个静态属性,是全局唯一的,就是前文所说的 pending-Reference list(挂起引用链表)的表头,该链表接收刚刚被 GC 回收了引用对象的引用实例,同时 ReferenceHandler 线程逐个将它们移出并处理(加入到用户引用队列中,或者直接执行其clean
方法),如果引用实例没有注册用户引用队列,GC 是不会将引用实例放入挂起引用链表。ReferenceHandler
即引用处理线程是个私有的静态内部类,该线程只会启动一个实例,Reference
在其类加载的时候会启动一个引用处理线程实例,引用处理线程是个高优先级守护线程,其作用就是处理被 GC 加入 pending-Reference list 的引用实例。
关于
java.lang.ref.Reference<T>
的属性queue
:如果在创建引用实例的时候注册了该队列(就是调用构造函数Reference(T referent, ReferenceQueue<? super T> queue)
创建的引用实例,并且queue
参数不为空),那么 GC 在回收了引用实例的引用对象后,会将该引用实例加入挂起引用链表(挂起引用链表就是pending
链表,该操作由 GC 线程完成),引用处理线程会不断轮询pending
,如果pending
的当前引用实例是Cleaner
类的实例,那么执行clean()
方法,否则将其转移到它注册的queue
里面。
图3:引用实例与引用队列
4. Reference与GC协作
上面我们讲了,由Reference
及其子类定义的引用类型是特殊引用类型,他们同GC紧密协作,会被GC进行特殊对待,那么我们现在看看GC如何特殊处理 Reference
实例的。
GC在扫描期会将发现的引用实例加入到由 JVM 维护的 DiscoveredList(发现列表)里,在后期,GC 专门有两个阶段来处理 DiscoveredList 中的引用实例,分别是引用处理 Ref Proc 和引用排队 Ref Enq 阶段:
- 引用处理 Ref Proc:这个阶段,GC 处理 DiscoveredList 中引用实例的引用对象(就是
Reference
的referent
指向的对象),如果引用对象没有其他强引用链,那么会根据引用的类型,对这些引用进行响应的处理(比如对于弱引用和虚引用,将referent
属性设为空,这样关联的引用对象就会变为彻底不可达,被 GC 回收) - 引用排队 Ref Enq:这个阶段,引用对象被回收的引用实例会被 GC 放入 pending-Reference list ,就是
Reference
内的pending
队列
ReferenceHandler 这个后台线程会不断的轮询 pending
,将被加入 pending
队列引用实例放入期注册的队列,如果是该引用实例是 sun.misc.Cleaner
类的实例,那么会调用其 clean
方法。
5. 特殊引用类型的应用
5.1 FinalReference
5.1 SoftReference 应用
5.2 WeakReference 应用
java.lang.ref.WeakReference<T>
弱引用,很多书上说,java.lang.ref.WeakReference<T>
适合用来做缓存,但是 WeakReference
并不适合直接用于缓存(特大对象除外,比如 Android 应用中的图片瀑布流,每一个图片对象可以使用 WeakReference
),因为,弱可达对象只能存活到下一次 GC,所以如果用 WeakReference
做缓存,对象的生命周期太短,而且被缓存的对象无论其使用频率如何,都会在下一次 GC 被回收。且缓存对象的回收时机完全由垃圾收集器下一次的 GC 时间决定,对于用户来说不可控。
5.3 PhantomReference 应用
java.lang.ref.PhantomReference<T>
是虚引用
###5.4 sun.misc.Cleaner 引用