第4讲 强引用、软引用、弱引用、幻象引用有什么区别?

强引用、软引用、弱引用、幻象引用有什么区别?具体使用场景是什么?

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

0. 历史原因:
引用出现的根源是由于GC内存回收的基本原理—GC回收内存。
本质上是回收对象,而目前比较流行的回收算法是可达性分析算法,从GC Roots开始按照一定的逻辑判断一个对象是否可达,不可达的话就说明这个对象已死(除此之外另外一种常见的算法就是引用计数法,但是这种算法有个问题就是不能解决相互引用的问题)。
基于此Java向用户提供了四种可用的引用。同时还提供了一种不可被使用的引用—FinalReference,这个引用是和析构函数密切相关的)。

1. 所有引用类型,都是抽象类 java.lang.ref.Reference 的子类。通过 其get()方法获取原对象。

  • 所谓强引用(“Strong” Reference),最常见的普通对象引用,线程直接可达,只要还有强引用指向一个对象,就能表明对象还“活着”,JVM宁愿抛出OutOfMemoryError运行时错误(OOM),使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题。
    • 对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,当然具体回收时机还是要看垃圾收集策略。
    • 平常典型编码Object obj = new Object()中的obj就是强引用
  • 软引用(SoftReference),是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,抛出 OutOfMemoryError 之前,清理软引用指向的对象
    • 软引用通常用来实现内存敏感的缓存。如图片缓存框架中,“内存缓存”中的图片是以这种引用来保存,使得JVM在发生OOM之前,可以回收这部分缓存。
    • 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中,执行后续逻辑。
  • 弱引用(WeakReference)并不能使对象豁免垃圾收集,仅仅是提供一种访问在弱引用状态下对象的途径。这就可以用来构建一种没有特定约束的关系,比如,维护一种非强制性的映射关系,如果试图获取时对象还在,就使用它,否则重现实例化
    • 弱引用同样是很多缓存实现的选择。
    • Java的TheardLocal动态代理都是基于这样的一个引用实现的,一般针对那些比较敏感的数据。
    • 在RPC调用中(?没验证过,我的理解)。例如,一个类发送网络请求,承担callback的静态内部类,则常以弱引用的方式来保存外部类(宿主类)的引用,当外部类需要被JVM回收时,不会因为网络请求没有及时回来,导致外部类不能被回收,引起内存泄漏,如果被回收了,重现实例化该外部类。
    • 由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。
    • 弱引用可以和一个引用队列(ReferenceQueue)联合使用,感觉弱引用和软引用区别不大?
    • “弱引用也许为了一些工具类在设计时又要考虑易用性,又要尽量防止开发者编程不当造成内存泄漏”,比如ThreadLocalMap的Entry代码,Entry弱引用了ThreadLocal<?>,就不会由于Entry本身一直存在使得对应的ThreadLocal<?>实例一直无法回收(这种通常是ThreadLocal被worker线程引用了,worker不会停的)
  • 对于幻象引用(PhantomReference),有时候也翻译成虚引用,你不能通过它访问对象。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。幻象引用仅仅是提供了一种确保对象已经被 finalize 以后,使用幻象引用做某些事情的机制,比如,通常用来做所谓的 Post-Mortem 清理机制,如Java 平台自身 Cleaner 机制等。
    • 利用幻象引用可以监控对象的创建和销毁
    • 幻象引用 get 方法只返回 null。

2. Java 定义的不同可达性级别(reachability level)

  • 强可达(Strongly Reachable),就是当一个对象可以有一个或多个线程可以不通过各种引用访问到的情况。比如,new创建一个对象,那么创建它的线程对它就是强可达
  • 软可达(Softly Reachable),就是当我们只能通过软引用才能访问到对象的状态。
  • 弱可达(Weakly Reachable)无法通过强引用或者软引用访问,只能通过弱引用访问时的状态。
  • 幻象可达(Phantom Reachable)上面流程图已经很直观了,就是没有强、软、弱引用关联,并且 finalize 过了,只有幻象引用指向这个对象的时候。
  • 不可达(unreachable),意味着对象可以被清除了。

3. 可达性的变化

垃圾收集器可能会存在二次确认的问题,以保证处于弱引用状态的对象,没有改变为强引用。

4. 内存泄漏可能的原因:

  • 错误的保持了强引用(比如,将get软引用的对象,赋值给了 static 变量)
  • ……待丰富,欢迎告知

5.JVM 垃圾收集
软引用通常会在最后一次引用后,还能保持一段时间。
默认值是根据堆剩余空间计算的(以 M bytes 为单位)

-XX:SoftRefLRUPolicyMSPerMB=3000 

JDK 8 诊断 JVM 引用情况HotSpot JVM 自身便提供了选项(PrintReferenceGC

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintReferenceGC 

引用统计 片段:

0.403: [GC (Allocation Failure) 0.871: [SoftReference, 0 refs, 0.0000393 secs]0.871: [WeakReference, 8 refs, 0.0000138 secs]0.871: [FinalReference, 4 refs, 0.0000094 secs]0.871: [PhantomReference, 0 refs, 0 refs, 0.0000085 secs]0.871: [JNI Weak Reference, 0.0000071 secs][PSYoungGen: 76272K->10720K(141824K)] 128286K->128422K(316928K), 0.4683919 secs] [Times: user=1.17 sys=0.03, real=0.47 secs] 

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

6. 引用队列(ReferenceQueue)
使用软/弱/幻象 引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个引用加入到与之关联的引用队列中。
后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。

7.Reachability Fence
使用JEP 193: Variable Handles 的一部分,将 Java 平台底层的一些能力暴露出来:

static void reachabilityFence(Object ref) 

解决对象没有被强引用,而依赖对象的部分属性的情况,需要通知 JVM 对象是在被使用的的问题。

在 JDK 源码中,reachabilityFence 大多使用在 Executors 或者类似新的 HTTP/2 客户端代码中,大部分都是异步调用的情况。

JDK11 示意代码 使用方法:

 class Resource {
   private static ExternalResource[] externalResourceArray = ...
   int myIndex;
   Resource(...) {
     myIndex = ...
     externalResourceArray[myIndex] = ...;
     ...
   }
   protected void finalize() {
     externalResourceArray[myIndex] = null;
     ...
   }
   public void action() {
     try {
       // ...
       int i = myIndex;
       Resource.update(externalResourceArray[i]);
     } finally {
       Reference.reachabilityFence(this);
     }
   }
   private static void update(ExternalResource ext) {
     ext.status = ...;
   }
 }

将需要 reachability 保障的代码段利用 try-finally 包围起来,在 finally 里明确声明对象强可达

有人提问:new Resource().action()那里我以前一直以为,对象的方法在运行期间一定会持有this引用,间接使得对象的field可达不会被回收。现在看来是错的?
作者回复:按照Java语言规范发生回收是合规的,极端情况会出现field不可用情况。例如,如果没有使用reachabilityFence,执行new Resource().action() 没有强引用指向创建出来的 Resource 对象,JVM 对它进行 finalize 操作是完全合法的。

真实有关reachability的例子:也是reachabilityFence的一个使用http://mail.openjdk.java.net/pipermail/jdk-dev/2018-October/002067.html清楚说明对方法的调用并不能保证对象存活


扩展:

  • 翻阅Executors 或者类似新的 HTTP/2 客户端源码,Reachability Fence相关实例;
  • 编写图片缓存框架;
  • 翻阅主流缓存框架,验证软引用;
  • 翻阅ThreadLocal和动态代理源码,验证弱引用;
  • 作者在评论中多次提及阅读Cleaner机制代码,有助于理解;
  • 阅读评论区推荐的《Java Reference Objects》http://www.kdgregory.com/index.php?page=java.refobj

笔记整理自极客时间《Java核心技术36讲》专栏,仅做学习用途。
《Java核心技术36讲》 :前Oracle首席工程师,讲解Java核心技术原理 、 Java面试必考知识点 、 完整的Java知识体系 ,有深度有广度。
极客时间专栏《Java核心技术36讲》

发布者

jahentao

挖掘概念,创造工具