持续创造,加快生长!这是我参与「日新方案 10 月更文挑战」的第24天,点击检查活动详情

前语

Java 言语中,除了原始数据类型的变量,其他一切都是所谓的引证类型,指向各种不同的目标,理解引证关于把握 Java 目标生命周期和 JVM 内部相关机制非常有帮助。

本篇博文的重点是,强引证、软引证、弱引证、幻象引证有什么区别?详细运用场景是什么?

概述

不同的引证类型,首要表现的是目标不同的可达性(reachable)状况和对废物搜集的影响

强引证(“Strong” Reference),便是咱们最常见的一般目标引证,只需还有强引证指向一个目标,就能标明目标还“活着”,废物搜集器不会碰这种目标。关于一个一般的目标,假如没有其他的引证联系,只需超过了引证的作用域或许显式地将相应(强)引证赋值为 null,便是能够被废物搜集的了,当然详细收回机遇仍是要看废物搜集战略。

软引证(SoftReference),是一种相对强引证弱化一些的引证,能够让目标豁免一些废物搜集,只有当 JVM 以为内存不足时,才会去企图收回软引证指向的目标。JVM 会确保在抛出 OutOfMemoryError 之前,整理软引证指向的目标。软引证一般用来完成内存灵敏的缓存,假如还有空闲内存,就能够暂时保留缓存,当内存不足时整理掉,这样就确保了运用缓存的一起,不会耗尽内存。

弱引证(WeakReference)并不能使目标豁免废物搜集,仅仅是供给一种拜访在弱引证状况下目标的途径。这就能够用来构建一种没有特定束缚的联系,比方,维护一种非强制性的映射联系,假如企图获取时目标还在,就运用它,不然重现实例化。它同样是许多缓存完成的挑选。

幻象引证,有时分也翻译成虚引证,你不能经过它拜访目标。幻象引证仅仅是供给了一种确保目标被 finalize 以后,做某些作业的机制,比方,一般用来做所谓的 Post-Mortem 整理机制,也有人运用幻象引证监控目标的创建和毁掉。

正文

1、目标可达性状况流转剖析

【JAVA】强引用、软引用、弱引用、幻象引用有什么区别?

  • 强可达(Strongly Reachable),便是当一个目标能够有一个或多个线程能够不经过各种引证拜访到的状况。比方,咱们新创建一个目标,那么创建它的线程对它便是强可达。
  • 软可达(Softly Reachable),便是当咱们只能经过软引证才干拜访到目标的状况。
  • 弱可达(Weakly Reachable),相似前面说到的,便是无法经过强引证或许软引证拜访,只能经过弱引证拜访时的状况。这是非常临近 finalize 状况的机遇,当弱引证被清除的时分,就契合 finalize 的条件了。
  • 幻象可达(Phantom Reachable),上面流程图现已很直观了,便是没有强、软、弱引证相关,而且 finalize 过了,只有幻象引证指向这个目标的时分。
  • 当然,还有一个最终的状况,便是不可达(unreachable),意味着目标能够被清除了。

判断目标可达性,是 JVM 废物搜集器决定怎样处理目标的一部分考虑。

一切引证类型,都是抽象类 java.lang.ref.Reference 的子类,你可能留意到它供给了 get() 办法:

【JAVA】强引用、软引用、弱引用、幻象引用有什么区别?

除了幻象引证(由于 get 永久回来 null),假如目标还没有被毁掉,都能够经过 get 办法获取原有目标。这意味着,运用软引证和弱引证,咱们能够将拜访到的目标,从头指向强引证,也便是人为的改变了目标的可达性状况!

所以,关于软引证、弱引证之类,废物搜集器可能会存在二次承认的问题,以确保处于弱引证状况的目标,没有改变为强引证。

但是,你觉得这里有没有可能呈现什么问题呢?

不错,假如咱们错误的坚持了强引证(比方,赋值给了 static 变量),那么目标可能就没有机会变回相似弱引证的可达性状况了,就会产生内存走漏。所以,检查弱引证指向目标是否被废物搜集,也是确诊是否有特定内存走漏的一个思路,假如咱们的结构运用到弱引证又置疑有内存走漏,就能够从这个视点检查。

2、引证行列(ReferenceQueue)运用

谈到各种引证的编程,就必定要说到引证行列。咱们在创建各种引证并相关到相应目标时,能够挑选是否需求相关引证行列,JVM 会在特定机遇将引证 enqueue 到行列里,咱们能够从行列里获取引证(remove 办法在这里实践是有获取的意思)进行相关后续逻辑。尤其是幻象引证,get 办法只回来 null,假如再不指定引证行列,基本就没有意义了。看看下面的示例代码。运用引证行列,咱们能够在目标处于相应状况时(关于幻象引证,便是前面说的被 finalize 了,处于幻象可达状况),履行后期处理逻辑。

Object counter = new Object();
ReferenceQueue refQueue = new ReferenceQueue<>();
PhantomReference<Object> p = new PhantomReference<>(counter, refQueue);
counter = null;
System.gc();
try {
    // Remove是一个堵塞办法,能够指定timeout,或许挑选一向堵塞
    Reference<Object> ref = refQueue.remove(1000L);
    if (ref != null) {
        // do something
    }
} catch (InterruptedException e) {
    // Handle it
}

3、显式地影响软引证废物搜集

前面泛泛说到了引证对废物搜集的影响,尤其是软引证,究竟 JVM 内部是怎样处理它的,其实并不是非常清晰。那么咱们能不能运用什么办法来影响软引证的废物搜集呢?

答案是有的。软引证一般会在最终一次引证后,还能坚持一段时间,默认值是依据堆剩下空间核算的(以 M bytes 为单位)。从 Java 1.3.1 开始,供给了 -XX:SoftRefLRUPolicyMSPerMB 参数,咱们能够以毫秒(milliseconds)为单位设置。比方,下面这个示例便是设置为 3 秒(3000 毫秒)。

-XX:SoftRefLRUPolicyMSPerMB=3000

这个剩下空间,其实会受不同 JVM 形式影响,关于 Client 形式,比方一般的 Windows 32 bit JDK,剩下空间是核算当前堆里空闲的巨细,所以愈加倾向于收回;而关于 server 形式 JVM,则是依据 -Xmx 指定的最大值来核算。

本质上,这个行为仍是个黑盒,取决于 JVM 完成,即使是上面说到的参数,在新版的 JDK 上也未必有用,另外 Client 形式的 JDK 现已逐渐退出历史舞台。所以在咱们运用时,能够参考相似设置,但不要过于依赖它。

4、确诊 JVM 引证状况

假如你置疑运用存在引证(或 finalize)导致的收回问题,能够有许多东西或许选项可供挑选,比方 HotSpot JVM 自身便供给了清晰的选项(PrintReferenceGC)去获取相关信息,我指定了下面选项去运用 JDK 8 运行一个样例运用:

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

这是 JDK 8 运用 ParrallelGC 搜集的废物搜集日志,各种引证数量非常清晰。

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 现已不再存在,我在专栏后面的废物搜集主题里会愈加系统的阐述。

5、Reachability Fence

除了我前面介绍的几种基本引证类型,咱们也能够经过底层 API 来达到强引证的作用,这便是所谓的设置 reachability fence

为什么需求这种机制呢?考虑一下这样的场景,按照 Java 言语规范,假如一个目标没有指向强引证,就契合废物搜集的标准,有些时分,目标自身并没有强引证,但是或许它的部分属性还在被运用,这样就导致诡异的问题,所以咱们需求一个办法,在没有强引证状况下,告诉 JVM 目标是在被运用的。说起来有点绕,咱们来看看 Java 9 中供给的事例。

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 {
     // 调用reachbilityFence,清晰保证目标strongly reachable
     Reference.reachabilityFence(this);
 }
 }
 private static void update(ExternalResource ext) {
    ext.status = ...;
 }
} 

办法 action 的履行,依赖于目标的部分属性,所以被特定维护了起来。不然,假如咱们在代码中像下面这样调用,那么就可能会呈现困扰,由于没有强引证指向咱们创建出来的 Resource 目标,JVM 对它进行 finalize 操作是彻底合法的。

new Resource().action()

相似的书写结构,在异步编程中似乎是很普遍的,由于异步编程中往往不会用传统的“履行 -> 回来 -> 运用”的结构。

在 Java 9 之前,完成相似功能相对比较繁琐,有的时分需求采取一些比较隐晦的小技巧。幸亏,java.lang.ref.Reference 给咱们供给了新办法,它是 JEP 193: Variable Handles 的一部分,将 Java 平台底层的一些才能暴露出来:

static void reachabilityFence(Object ref)

在 JDK 源码中,reachabilityFence 大多运用在 Executors 或许相似新的 HTTP/2 客户端代码中,大部分都是异步调用的状况。编程中,能够按照上面这个比如,将需求 reachability 保证的代码段运用 try-finally 包围起来,在 finally 里清晰声明目标强可达。

跋文

以上便是【JAVA】# 强引证、软引证、弱引证、幻象引证有什么区别?的一切内容了;

总结了 Java 言语供给的几种引证类型、相应可达状况以及关于 JVM 作业的意义,并剖析了引证行列运用的一些实践状况,最终介绍了在新的编程形式下,怎样运用 API 去保证目标不被意外收回,希望对你有所帮助。

上篇精讲:【JAVA】并发类库供给的线程池有哪几种?

我是,等待你的重视;

创造不易,请多多支持;

系列专栏:面试精讲 JAVA