写点什么

剖析 JDK:强引用、软引用、弱引用、虚引用有何区别?

发布于: 2021 年 01 月 08 日
剖析JDK:强引用、软引用、弱引用、虚引用有何区别?

1 写在开头

1.1 JVM 内存模型

开讲前,我们先回顾下 JVM 的基本结构。根据《Java 虚拟机规范(Java SE 7 版)》。

JVM 的内存管理包括以下几个运行时数据区域:

  • 程序计数器(Program Counter Register):当前线程执行的字节码指示器

  • Java 虚拟机栈(Java Virtual Machine Stacks):Java 方法执行的内存模型,每个方法会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

  • 本地方法栈(Native Method Stack):(虚拟机使用到的)本地方法执行的内存模型。

  • Java 堆(Java Heap):虚拟机启动时创建的内存区域,唯一目的是存放对象实例,处于逻辑连续但物理不连续内存空间中。

  • 方法区(Method Area):存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

  • 运行时常量池(Runtime Constant Pool):方法区的一部分,存放编译器生成的各种字面值和符号引用。


1.2 可达性算法


本文即将介绍到的:Java 的强引用、软引用、弱引用、虚引用,都与 JVM 的 GC 有着莫大的关系。

我们都知道,常用的 GC 回收关注的 JVM 区域为是 Java 堆(包含了方法区{外界也称为“常量池”}),而 我们的引用(无论是强/软/弱/虚)都是指向于堆的某一块内存区域。Java 堆与方法区,只有在程序运行期间才知道会创建哪些对象,这部分内存的分配和回收都是动态的。


主流商用程序语言的主流实现中,都是通过可达性分析(Reachability Analysis)来判定对象是否存活。可达性算法基本思路是:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜素走过的路径称为“Reference Chain”(引用链),当下一个对象到 GC Roots 没有任何引用链相连时,则证明该对象不可用。



2 Java 对象引用是什么

进入今天的主题:再谈引用是什么。无论是何种 GC 回收算法,判断对象的存活都与“引用”有关。在 SDK1.2 之前,Java 对引用定义很传统:如果 reference 类型的数据存储的数值代表的是另外一块内存的起始地址,就称为“这块内存代表着一个引用”。


于是,在 JDK1.2 之后,Java 对引用概念进行了扩充,把引用分类为:

  1. 强引用(Strong Reference)

  2. 软引用(Soft Reference)

  3. 弱引用(Weak Reference)

  4. 虚引用(Phantom Reference)

4 种引用类型,它们的引用强度逐渐减弱。


3 强引用(Strong Reference)

在程序代码中普遍存在的,类似下面这类的引用,只要强引用存在,那么 GC Collector 就永远不会回收掉被引用的对象。


Object obj = new Object();
复制代码


  • 例子 1



/** * <p> * 强引用:当对象指向内存为空不关联时,才会被GC回收,否则永远不会回收。 * </p> */
public class ObjectReference { public static void main(String[] args) { String str = "StrongReference"; System.out.println(str); System.gc(); System.out.println(str); str = null; System.out.println(str); }}
复制代码


  • 输出


StrongReferenceStrongReferencenull
复制代码


4 软引用(Soft Reference)

描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统即将要发生内存溢出异常之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。JDK1.2 之后,提供了 SoftReference 类实现软引用。


  • 例子 2(加入虚拟机启动参数:-Xms10m -Xmx40m,表示 JVM 最大堆内存为 12M)


/** * <p> *     软引用:当内存不足时,才会触发JVM进行GC回收。 *       适合做缓存。 * </p> */public class SoftReferenceObject1 {
public static void main(String[] args) { softRefenceTest(); } /** * 仅仅gc, 还不会回收。需内存不够 GC的时候才回收软引用。 * 启动参数: * -Xmx16m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:NewSize=2m -XX:MaxNewSize=2m */ private static void softRefenceTest() { final int _8M = 8 * 1024 * 1024; List<SoftReference> list = new ArrayList<SoftReference>(); System.out.println("add 8m -1"); list.add(new SoftReference(new byte[_8M])); System.out.println("add 8m -2"); list.add(new SoftReference(new byte[_8M])); System.out.println("add 8m -3"); list.add(new SoftReference(new byte[_8M])); System.out.println("add 8m -4"); list.add(new SoftReference(new byte[_8M])); System.out.println("add 8m -5"); list.add(new SoftReference(new byte[_8M])); System.out.println("add 8m -6"); list.add(new SoftReference(new byte[_8M])); System.gc(); list.stream().forEach(r-> System.out.println(r.get())); }}
复制代码


  • 输出



add 8m -1add 8m -2add 8m -3add 8m -4add 8m -5add 8m -6nullnullnull[B@5f4da5c3[B@443b7951[B@14514713
复制代码


  • 代码解析

启动程序时,设置堆内存最大为40M;(-Xms10m -Xmx40m)而后程序依次申请分配8M的数组连续存储空间,经过6次创建对象操作,在5次时,已经达到堆内存上限40M了,因此触发了JVM的GC回收,导致此前4次创建的软引用所指向的堆内存对象被全部回收;然后第5/6次创建的对象依旧能够创建完成,因为此时内存共耗损16M,还没达到内存上限,因此软引用的对象可以继续存活。
复制代码


5 弱引用(Weak Reference)


弱引用:用于描述非必需对象,但它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次 GC 发生之前。当 GC 发生时,无论内存是否足够,都会回收掉只被弱引用关联的对象。JDK1.2 之后,提供了 WeakReference 类实现软引用。  


  • 例子 3



/** * <p> * 弱引用:无论内存是否足够,都会被GC回收 * </p> */
public class WeakReferenceObject { public static void main(String[] args) { WeakReference weakReference = new WeakReference(new long[1024*1024]); System.out.println(weakReference.get()); System.gc(); System.out.println(weakReference.get()); }}
复制代码


  • 输出



[J@312b1daenull
复制代码


我们开发中有个常见的缓存泄漏原因:一旦将对象引用放到缓存中,它很容易被遗忘掉,从而使得它不再有用并长期停留在缓存。


解决方法:使用 WeakHashMap 代表缓存,当缓存过期后会被自动删除,。参考《弱引用是什么》


下面是节选部分 WeakHashMap 的源码:元素队列的每个 Entry 元素,继承于 WeakReference。

    /**     * Reference queue for cleared WeakEntries     */    private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
/** * The entries in this hash table extend WeakReference, using its main ref * field as the key. */ private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> { V value; final int hash; Entry<K,V> next; }
复制代码


6 虚引用(Phantom Reference)

幽灵引用或幻影引用,是最弱的一种引用关系。一个对象是否有虚引用,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的,就是能够在这个对象被 GC 回收时,收到一个系统通知。JDK1.2 之后,提供了 PhantomReference 类实现软引用。


  • 例子 4



/** * <p> * 虚引用/幻影引用: * 1)无法通过虚引用获取对一个对象的真实引用 * 2)虚引用必须通过与 PhantomReference 组合一起使用。当GC 回收一个对象,发现它还有虚引用,就会在回收前把这个引用加到与之关联的ReferenceQueue中 * NIO运用到它管理堆外存。 * </p> */
public class ReferenceQueueObject { public static void main(String[] args) { ReferenceQueue referenceQueue = new ReferenceQueue(); PhantomReference<byte[]> reference = new PhantomReference<byte[]>(new byte[1], referenceQueue); System.out.println(reference.get()); }}
复制代码


  • 输出


null
复制代码


7 总结

我们用一张图来总结 Java 引用类型的使用场景与特征,学会准确使用 Java 的引用,将能给代码的性能带来很大的提升。以上便是这次分享的全部内容了。



发布于: 2021 年 01 月 08 日阅读数: 57
用户头像

Diligence is the mother of success. 2018.03.28 加入

公众号:后台技术汇 笔者主要从事Java后台开发,喜欢技术交流与分享,保持饥渴,一起进步!

评论

发布
暂无评论
剖析JDK:强引用、软引用、弱引用、虚引用有何区别?