不同的引用类型,主要体现的是对象不同的可达性(reachable)状态和对垃圾收集的影响

强引用

是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还”活着”,垃圾收集器就不会碰这种对象。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,当然具体回收时机还是要看垃圾收集策略。

软引用

是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

弱引用

弱引用(WeakReference) 并不能使对象豁免垃圾收集,仅仅是提供一种访问在弱引用状态下对象的途径。这就可以用来构建一种没有特定约束的关系,比如维护一种非强制性的映射关系,如果试图获取时对象还在,就使用它,否则重现实例化。它同样是很多缓存实现的选择。

幻象引用

有时也翻译成虚引用,你不能通过它访问对象。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。比如通常用来做所谓的 Post-Mortem 清理机制,如Cleaner机制等,也有人利用幻象引用监控对象的创建和销毁。

充分理解这些引用,对于我们设计可靠的缓存等框架,或者诊断应用 OOM 等问题,会很有帮助。比如,诊断 MySQL connector-j 驱动在特定模式下(useCompression = trye) 的内存泄漏问题,就需要我们理解怎么排查幻象引用的堆积问题。

扩展

1. 对象可达性状态流转分析

下面的图是对象生命周期和不同可达性状态,以及不同状态可能的改变关系。
04.强引用、软引用、弱引用、幻象引用有什么区别? - 图1

  • 强可达(Strongly Reachable) ,就是当一个对象可以有一个或多个线程可以不通过各种引用访问到的情况。比如,我们新创建一个对象,那么创建它的线程对它就是强可达。
  • 软可达(Softly Reachable),就是当我们只能通过软引用才能访问到对象的状态。
  • 弱可达(Weakly Reachable),无法通过强引用或软引用访问,只能通过弱引用访问时的状态。这十分临近 finalize 状态的时机,当弱引用被清除的时候,就符合 finalize 的条件了。
  • 幻象可达(Phantom Reachable), 没有强、软、弱引用关联,并且 finalize 过了,只有幻象引用指向这个对象的时候。
  • 最后一个状态,不可达(unreachable),意味着对象可被清除了。

判断对象可达性,是JVM 垃圾收集器决定如何处理对象的一部分考虑。
所有引用类型,都是抽象类 java.lang.ref.Reference 的子类,都提供了 get() 方法。除了幻象引用(因为 get 永远返回 null),如果对象还没被销毁,都可通过 get 方法获取原有对象。这意味着,利用软引用和弱引用,我们可将访问到的对象,重新指向强引用,也就是人为地改变了对象的可达性状态。
所以,对于软引用、弱引用之类,垃圾收集器可能会存在二次确定问题,以保证处于弱引用状态的对象,没有改变为强引用。

这里存在一个问题:如果我们错误地保持了强引用(如赋值给了static变量),那么对象可能就没有机会变回类似弱引用的可达性状态了,就会产生内存泄漏。所以检查弱引用指向对象是否被垃圾收集,也是诊断是否有特定内存泄漏的一个思路,如果我们的框架使用到弱引用又怀疑有内存泄漏,就可以从这个角度检查。

2. 引用队列(ReferenceQueue) 使用

我们在创建各种引用并关联到响应对象时,可以选择是否需要关联引用队列,JVM 会在特定时机将引用 enqueue 到队列里,我们可以从队列里获取引用(remove 方法在这里实际是有获取的意思)进行相关后续逻辑。尤其是幻象引用,get方法只返回 null,如果再不指定引用队列,基本就没有意义了。下面的代码,利用引用队列,我们可在对象处于相应状态时(对于幻象引用,就是前面说的被 finalize了,处于幻想可达状态),执行后期处理逻辑。

  1. Object counter = new Object();
  2. ReferenceQueue refQueue = new ReferenceQueue<>();
  3. PhantomReference<Object> p = new PhantomReference<>(counter, refQueue);
  4. counter = null;
  5. System.gc();
  6. try {
  7. // remove 是一个阻塞方法,可以指定 timeout,或者选择一直阻塞
  8. Reference<Object> ref = refQueue.remove(1000L);
  9. if (ref != null) {
  10. // do something
  11. }
  12. } catch (InterruptedException e) {
  13. // handle it
  14. }

3. 显式地影响软引用垃圾收集

我们能不能使用什么方法来影响软引用的垃圾收集呢?
软引用通常在最后一次引用后,还能保持一段时间,默认值是根据堆剩余空间计算的(以 M bytes 为单位)。从 Java 1.3.1 开始,提供了 -XX:SoftRefLRUPolicyMSPerMB 参数,我们可以以毫秒(milliseconds) 为单位设置。下面是设置为3秒(3000毫秒)。

  1. -XX:SoftRefLRUPolicyMSPerMB = 3000

这个剩余空间,会受不同 JVM模式影响,对于 Client 模式,比如通常的 windows 32 bit JDK,剩余空间是计算当前堆里空闲的大小,所以更加倾向于回收;而对于 server 模式JVM,则是根据 -Xmx 指定的最大值来计算。

4. 诊断 JVM 引用情况

HotSpot JVM 自身提供了明确的选项去获取相关信息:

  1. -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintReferenceGC

注意:JDK 9 对 JVM 和垃圾收集日志进行了广泛重构, 类似 PrintGCTimeStamps 和 PrintReferenceGC 已经不存在。

5. Reachability Fence

我们还可通过底层 API 来达到强引用的效果,即设置 reachability rence
如果一个对象没有指向强引用,就符合垃圾收集的标准,有时候,对象本身并没有强引用,但也许它的部分属性还在被使用,这就导致了诡异的问题。所以我们需要一个方法,在没有强引用的情况下,通知 JVM 对象是在被使用的。
在 JDK 源码中,reachabilityFence 大多使用在 Exectors 或者 类似新的 HTTP/2 客户端代码中,大部分都是异步调用的情况。编码中,将需要 reachability 保障的代码段利用 try-finally 包围起来,在 finally 里明确声明对象强可达。