Java中一共有4种引用类型(其实还有一些其他的引用类型比如FinalReference):强引用、软引用、弱引用、虚引用。其中强引用就是我们经常使用的 Objecta=newObject(); 这样的形式,在Java中并没有对应的Reference类。
本篇文章主要是分析软引用、弱引用、虚引用的实现,这三种引用类型都是继承于Reference这个类,主要逻辑也在Reference中。
问题
在分析前,先抛几个问题?
1.网上大多数文章对于软引用的介绍是:在内存不足的时候才会被回收,那内存不足是怎么定义的?什么才叫内存不足?
2.网上大多数文章对于虚引用的介绍是:形同虚设,虚引用并不会决定对象的生命周期。主要用来跟踪对象被垃圾回收器回收的活动。真的是这样吗?
3.虚引用在Jdk中有哪些场景下用到了呢?
我们先看下 Reference.java中的几个字段
一个Reference对象的生命周期如下:
*主要分为Native层和Java层两个部分。
- Native层在GC时将需要被回收的Reference对象加入到DiscoveredList中(代码
在 referenceProcessor.cpp中 process_discovered_references方法),然后将DiscoveredList的元素移动到PendingList中(代码在 referenceProcessor.cpp中 enqueue_discovered_ref_helper方法),PendingList的队首就是Reference类中的pending对象。
看看Java层的代码
流程比较简单:就是源源不断的从PendingList中提取出元素,然后将其加入到ReferenceQueue中去,开发者可以通过从ReferenceQueue中poll元素感知到对象被回收的事件。
另外需要注意的是,对于Cleaner类型(继承自虚引用)的对象会有额外的处理:在其指向的对象被回收时,会调用clean方法,该方法主要是用来做对应的资源回收,在堆外内存DirectByteBuffer中就是用Cleaner进行堆外内存的回收,这也是虚引用在java中的典型应用。
看完了Reference的实现,再看看几个实现类里,各自有什么不同。
SoftReference
软引用的实现很简单,就多了两个字段: clock和 timestamp。 clock是个静态变量,每次GC时都会将该字段设置成当前时间。 timestamp字段则会在每次调用get方法时将其赋值为 clock(如果不相等且对象没被回收)。
那这两个字段的作用是什么呢?这和软引用在内存不够的时候才被回收,又有什么关系呢?
这些还得看JVM的源码才行,因为决定对象是否需要被回收都是在GC中实现的。
refs_lists中存放了本次GC发现的某种引用类型(虚引用、软引用、弱引用等),而 process_discovered_reflist方法的作用就是将不需要被回收的对象从 refs_lists移除掉,refs_lists最后剩下的元素全是需要被回收的元素,最后会将其第一个元素赋值给上文提到过的 Reference.java#pending字段。
ReferencePolicy一共有4种实现:NeverClearPolicy,AlwaysClearPolicy,LRUCurrentHeapPolicy,LRUMaxHeapPolicy。。其中NeverClearPolicy永远返回false,代表永远不回收SoftReference,在JVM中该类没有被使用,AlwaysClearPolicy则永远返回true,在 referenceProcessor.hpp#setup方法中中可以设置policy为AlwaysClearPolicy,至于什么时候会用到AlwaysClearPolicy,大家有兴趣可以自行研究。
LRUCurrentHeapPolicy和LRUMaxHeapPolicy的shouldclearreference方法则是完全相同:
timestmp_clock就是SoftReference的静态字段 clock,java_lang_ref_SoftReference::timestamp(p)对应是字段 timestamp。如果上次GC后有调用 SoftReference#get, interval值为0,否则为若干次GC之间的时间差。
_max_interval则代表了一个临界值,它的值在LRUCurrentHeapPolicy和LRUMaxHeapPolicy两种策略中有差异。
其中 SoftRefLRUPolicyMSPerMB默认为1000,前者的计算方法和上次GC后可用堆大小有关,后者计算方法和(堆大小-上次gc时堆使用大小)有关。
看到这里你就知道SoftReference到底什么时候被被回收了,它和使用的策略(默认应该是LRUCurrentHeapPolicy),堆可用大小,该SoftReference上一次调用get方法的时间都有关系。
WeakReference
![](https://upload-images.jianshu.io/upload_images/315466-c1f84335dba16600.png?imageMogr2/auto-
orient/strip%7CimageView2/2/w/1240)
可以看到WeakReference在Java层只是继承了Reference,没有做任何的改动。那referent字段是什么时候被置为null的呢?要搞清楚这个问题我们再看下上文提到过的 process_discovered_reflist方法:
不管是弱引用还是其他引用类型,将字段referent置null的操作都发生在 process_phase3中,而具体行为是由 clear_referent的值决定的。而 clear_referent的值则和引用类型相关。
可以看到,对于Soft references和Weak references clear_referent字段传入的都是true,这也符合我们的预期:对象不可达后,引用字段就会被置为null,然后对象就会被回收(对于软引用来说,如果内存足够的话,在Phase 1,相关的引用就会从refslist中被移除,到Phase 3时refslist为空集合)。
但对于Final references和 Phantom references, clear_referent字段传入的是false,也就意味着被这两种引用类型引用的对象,如果没有其他额外处理,只要Reference对象还存活,那引用的对象是不会被回收的。Final references和对象是否重写了finalize方法有关,不在本文分析范围之内,我们接下来看看Phantom references。
PhantomReference
可以看到虚引用的get方法永远返回null,我们看个demo。
从以上代码中可以看到,虚引用能够在指向对象不可达时得到一个'通知'(其实所有继承References的类都有这个功能),需要注意的是GC完成后,phanRef.referent依然指向之前创建Object,也就是说Object对象一直没被回收!
而造成这一现象的原因在上一小节末尾已经说了: 对于Finalreferences和Phantomreferences,clear_referent 字段传入的时false,也就意味着被这两种引用类型引用的对象,如果没有其他额外处理,在GC中是不会被回收的。
对于虚引用来说,从 refQueue.remove();得到引用对象后,可以调用 clear方法强行解除引用和对象之间的关系,使得对象下次可以GC时可以被回收掉。
End
针对文章开头提出的几个问题,看完分析,我们已经能给出回答:
1.我们经常在网上看到软引用的介绍是:在内存不足的时候才会回收,那内存不足是怎么定义的?为什么才叫内存不足?
软引用会在内存不足时被回收,内存不足的定义和该引用对象get的时间以及当前堆可用内存大小都有关系,计算公式在上文中也已经给出。
2.网上对于虚引用的介绍是:形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。主要用来跟踪对象被垃圾回收器回收的活动。真的是这样吗?
严格的说,虚引用是会影响对象生命周期的,如果不做任何处理,只要虚引用不被回收,那其引用的对象永远不会被回收。所以一般来说,从ReferenceQueue中获得PhantomReference对象后,如果PhantomReference对象不会被回收的话(比如被其他GC ROOT可达的对象引用),需要调用 clear方法解除PhantomReference和其引用对象的引用关系。