深入理解 Java 引用类型:强壮、柔软、脆弱、虚无的力量

简介: 深入理解 Java 引用类型:强壮、柔软、脆弱、虚无的力量

前言

引用是用于访问对象的变量,它是内存中的一个指针,指向堆中的对象实例;通过 reference 类型数据代表某块内存、某个对象的引用

若内存空间还足够时,仍然能保存在内存中,若内存空间在进行垃圾收集以后非常紧张下,就可以抛弃一些引用对象,因此,引出了四种不同的引用,分别是:强、软、弱、虚

Object obj = new Object();

一般在工作中都是使用如上所示的强引用,这种引用只有在内存空间不足时才会回收,并且该引用通过引用计数算法(Reference Count)或根可达算法(Root Searching)找不到与之相关联的引用情况下,随即在发生 GC 时这种强引用对象才会被回收

对象自我拯救

当引用计数为 0 或根不可达时,对象是在垃圾收集阶段被回收的,如何可以让对象可以自我完成救赎继续使用呢?可以通过重写的 finalize 方法来完成对象逃脱死亡的最后一次机会

若对象被判断为确实有必要执行 finalize 方法,那么该对象将会被放置在一个名为 F-Queue 队列之中,并会在后面由虚拟机自动建立的、低调度优先级的 Finalizer 线程去执行它们的 finalize 方法(所有类指向父类都是 Object,它承担了 finalize 方法定义)

低调度优先级线程去“执行”,当触发该方法运行时,但并不一定会等待它执行结束,例如:当 finalize 方法执行缓慢或更极端情况下发生了死循环!

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC ESCAPE_HOOK = null;
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method invoke");
        FinalizeEscapeGC.ESCAPE_HOOK = this;
    }
    public static void main(String[] args) throws InterruptedException {
        ESCAPE_HOOK = new FinalizeEscapeGC();
        ESCAPE_HOOK = null;
        System.gc();
        // 因为 finalize 方法优先级很低,暂停 0.5 s,以等待它
        TimeUnit.MILLISECONDS.sleep(500);
        // alive 拯救成功
        if (null != ESCAPE_HOOK) {
            System.out.println("FinalizeGC is alive!");
        } else {
            System.out.println("FinalizeGC is dead!");
        }
        System.gc();
        ESCAPE_HOOK = null;
        System.gc();
        // 因为 finalize 方法优先级很低,暂停 0.5 s,以等待它
        TimeUnit.MILLISECONDS.sleep(500);
        // dead 拯救失败
        if (null != ESCAPE_HOOK) {
            System.out.println("FinalizeGC is alive!");
        } else {
            System.out.println("FinalizeGC is dead!");
        }
        System.gc();
    }
}

以上代码演示了两点,如下:

  1. 对象可以在被 GC 时自我拯救
  2. 自救的机会只有一次,在 finalize 方法重新指向引用
  3. 一个对象的 finalize 方法最多只会被系统自动调用一次,如以上代码,第一次可以拯救成功,但第二次就无法拯救成功了

注意:实际工作中,-XX:+DisableExplicitGC 参数是开启的,禁用 System.gc() 调用,手动回收垃圾不会生效

finalize 出现,只是为了 Java 刚诞生时 C++ 程序员更容易接受 Java 所做出的一项妥协(C++ 需要手动 delete 删除引用指针、数组指针),而且它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,若要作关闭外部资源之类的工作时,完全可以使用 try/finally 块来完成,在 finally 代码块完成资源的释放工作

引用

接下来介绍不同引用之间的区别及联系

前置工作

/**
 * @author vnjohn
 * @since 2023/6/27
 */
public class FinalizeGC {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize method invoke");
    }
}

先提前准备一个资源类型,来判断是否发生了垃圾回收,若发生了会输出 finalize method invoke

强引用

/**
 * @author vnjohn
 * @since 2023/6/28
 */
public class StrongReference {
    public static void main(String[] args) throws Exception {
        FinalizeGC finalizeGC = new FinalizeGC();
        // finalizeGC = null;
        System.gc();
        // 因为 finalize 方法优先级很低,暂停 0.5 s,以等待它
        TimeUnit.MILLISECONDS.sleep(500);
    }
}

如上代码,定义实现了一个强引用对象,当我们调用 System.gc() 并不会进行回收,强引用对象只有在发生 GC 时并且该对象没有任何强引用关系链存在,就会被回收

System.gc() 不建议手动调用,垃圾回收的工作还是交由给专门负责这项工作的 JVM

通过:-XX:+DisableExplicitGC 参数来关闭手动 GC,默认是 - 开启的

软引用

软引用是用来描述一些还有用、非必须的对象,只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列入到回收范围之中,进行第二次的回收,若这次回收过后仍没有足够的内存,就会抛出内存溢出异常

在 Java 中,软引用通过 java.lang.ref.SoftReference 声明

private static final Integer MAX_BYTE = 1024 * 1024 * 10;
java.lang.ref.SoftReference<byte[]> softReference = new java.lang.ref.SoftReference<>(new byte[MAX_BYTE]);

软引用就是将对象使用 SoftReference 进行一层包装,当我们需要从软引用中获取到包装的对象时,直接调用 get 方法即可

private static final Integer MAX_BYTE = 1024 * 1024 * 10;
java.lang.ref.SoftReference<byte[]> softReference = new java.lang.ref.SoftReference<>(new byte[MAX_BYTE]);
System.out.println(softReference.get());

软引用特征:当内存不足时,会触发 GC 回收,若 GC 后内存还是不足够,就会回收掉软引用中包装的对象,也就是当 JVM 内存不足以使用时,才会回收该引用

通过代码来演示,当触发 GC 回收后,获取软引用包装的对象时是否会为空,如下:

/**
 * @author vnjohn
 * @since 2023/6/28
 */
public class SoftReference {
    private static final Integer MAX_BYTE = 1024 * 1024 * 10;
    private static final Integer PLUS_BYTE = 1024 * 1024 * 5;
    public static void main(String[] args) {
        // 实例化一个 10M 的类型为 SoftReference 的 m 对象
        java.lang.ref.SoftReference<byte[]> softReference = new java.lang.ref.SoftReference<>(new byte[MAX_BYTE]);
        System.gc();
        System.out.println(softReference.get());
        byte[] bytes = new byte[MAX_BYTE + PLUS_BYTE];
        System.out.println(softReference.get());
    }
}

先创建一个软引用包装的 10 M 字节数组对象,再创建一个 15 M 字节数组对象,若不对 JVM 参数作任何配置的话,运行是看不到任何效果的,先调整 JVM Options 参数如下:

# 最小、最大堆内存为 25 M
-Xms25M -Xmx25M

运行主 main 方法,执行结果如下:

[B@45ee12a7
null

基于以上结果,可以很清楚的看到通过手动 GC 方式回收时,软引用所包装的 byte 字节数组对象还存活好好的,但当我们又创建了一个 15 M 字节数组强引用对象后,堆内存不够了,所以就会将软引用包装的 byte 字节数组给回收掉了

软引用使用场景:非常适合用作缓存,当内存足够,可以正常的拿到缓存;当内存不够时,就会先干掉软引用缓存,不至于马上抛出 OOM 异常

弱引用

弱引用是用来描述哪些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只会生存到下一次垃圾收集发生为止;当垃圾收集开始工作,无论当前内存是否足够,都会回收掉哪些只被弱引用关联的对象

WeakReference

在 Java 中,弱引用通过 java.lang.ref.WeakReference 声明

弱引用的特点在于,无论内存是否足够,都会被回收,代码如下:

/**
 * @author vnjohn
 * @since 2023/6/28
 */
public class WeakReference {
    private static final Integer MAX_BYTE = 1024 * 1024 * 10;
    private static final Integer PLUS_BYTE = 1024 * 1024 * 5;
    public static void main(String[] args) {
        java.lang.ref.WeakReference<byte[]> weakReference = new java.lang.ref.WeakReference<>(new byte[MAX_BYTE]);
        System.out.println(weakReference.get());
        System.gc();
        System.out.println(weakReference.get());
        byte[] bytes = new byte[MAX_BYTE + PLUS_BYTE];
        System.out.println(weakReference.get());
    }
}

运行主 main 方法,执行结果如下:

[B@45ee12a7
null
null

基于以上结果,在未调用 System.gc() 方法手动触发垃圾收集时,弱引用包装的对象仍然是可见的,一旦触发了该方法调用,所有的弱引用包装对象都会被回收,无论你堆内存配置多大,弱引用包装的对象都会被当成垃圾回收掉,这是弱引用本身的特性

ThreadLocal

在使用弱引用时,有一个非常重要的类会在工作中经常使用 > ThreadLocal,大家应该都知道,它提供了一种方式,为每个线程能够拥有一份自己的变量副本,元素由内部类 ThreadMap 存储, ThreadMap 内部又有 Entry 类来作为数组存放变量值,Entry 继承至 WeakReference 弱引用

在 ThreadLocal 中,提供了三个常用的方法,如下:

public T get();
public void set(T value);
public void remove()

使用 ThreadLocal 时,要注意的是,remove 方法要配合 set 方法一起使用,因为在 Entry 结构中 Key 为弱引用所修饰,它会在垃圾收集时进行回收,但 Value 它为 Object 强引用会一直存放在内存中,即使发生垃圾收集也不会被回收;若这种情况下在流量比较大的时候,一直堆积这种强引用的无用对象,会造成内存泄漏,最终也有可能会发生 OOM 异常,导致系统不可用!!

基于以上这种要注意的情况,可以通过以下的方式去进行测试

/**
 * @author vnjohn
 * @since 2023/6/28
 */
public class ThreadLocalDemo {
    private static final Integer MAX_BYTE = 1024 * 1024 * 10;
    private static final Integer PLUS_BYTE = 1024 * 1024 * 5;
    static class ThreadLocalObject {
        private final byte[] bytes;
        public ThreadLocalObject(byte[] bytes) {
            this.bytes = bytes;
        }
        public byte[] getBytes() {
            return bytes;
        }
    }
    private static final ThreadLocal<ThreadLocalObject> CURRENT_THREAD_MAP = new ThreadLocal<>();
    public static void main(String[] args) {
        new Thread(() -> {
            ThreadLocalObject currentThreadObj = new ThreadLocalObject(new byte[MAX_BYTE]);
            CURRENT_THREAD_MAP.set(currentThreadObj);
            ThreadLocalObject threadLocalObject = CURRENT_THREAD_MAP.get();
            System.out.println(Thread.currentThread().getName() + ":" + threadLocalObject.getBytes());
        }, "Thread-A").start();
        new Thread(() -> {
            ThreadLocalObject currentThreadObj = new ThreadLocalObject(new byte[MAX_BYTE + PLUS_BYTE]);
            CURRENT_THREAD_MAP.set(currentThreadObj);
            ThreadLocalObject threadLocalObject = CURRENT_THREAD_MAP.get();
            System.out.println(Thread.currentThread().getName() + ":" + threadLocalObject.getBytes());
        }, "Thread-B").start();
    }
}

在 VM Options 配置好最小/最大堆内存大小,再进行测试:

# 最小、最大堆内存为 25 M
-Xms25M -Xmx25M

执行结果如下:

Exception in thread "Thread-B" java.lang.OutOfMemoryError: Java heap space
  at org.vnjohn.jvm.ThreadLocalDemo.lambda$main$1(ThreadLocalDemo.java:35)
  at org.vnjohn.jvm.ThreadLocalDemo$$Lambda$2/1989780873.run(Unknown Source)
  at java.lang.Thread.run(Thread.java:750)
Thread-A:[B@31c12e10

在 IDEA 工具开启阿里编码规范扫描时,就会提示我们 ThreadLocal 应该调用 remove 方法,所以在日常工作开发中,记得 set/remove 方法要配合使用,在 try/finally 块结合起来!!

InheritableThreadLocal

在这里,既然说到了 ThreadLocal,那么就再提出一个更有意思的知识点,如何在 Java 中子线程可以获取到父线程变量的值?

ThreadLocal 有一个子类 InheritableThreadLocal,通过它来实现可以获取父线程变量副本的值,示例代码如下:

/**
 * @author vnjohn
 * @since 2023/6/28
 */
public class InheritableThreadLocalDemo {
    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("Hello World");
        new Thread(() -> System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get()), "Thread-A").start();
        InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
        inheritableThreadLocal.set("Hello Vnjohn");
        new Thread(() -> System.out.println(Thread.currentThread().getName() + ":" + inheritableThreadLocal.get()), "Thread-B").start();
    }
}

以上代码,在线程:Thread-A,是获取不到 ThreadLocal 类设置的变量值的;只有在线程:Thread-B,通过 InheritableThreadLocal 设置,然后才可以获取到具体的变量值

InheritableThreadLocal 关于该 ThreadLocal 子类共享父线程变量副本的实现原理在后续文章分析,敬请期待!

虚引用

虚引用也称为 “幽灵引用” 或者 “幻影引用”,它是最弱的一种引用关系;一个对象是否有弱引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例;为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被垃圾收集器回收时收到一个系统通知

在 Java 中,虚引用通过 java.lang.ref.PhantomReference 声明

接下来,来看看虚引用如何使用,如下:

/**
 * @author vnjohn
 * @since 2023/6/28
 */
public class PhantomReference {
    public static void main(String[] args) throws IOException {
        ReferenceQueue<FinalizeGC> queue = new ReferenceQueue<>();
        List<Object> objects = new ArrayList<>();
        java.lang.ref.PhantomReference<FinalizeGC> phantomReference = new java.lang.ref.PhantomReference<>(new FinalizeGC(), queue);
        // 第一个线程:一直往集合中塞入数据,直至堆内存大小不足,会触发虚引用回收
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                objects.add(new byte[1024 * 1024]);
            }
            System.out.println(phantomReference.get());
        }, "Thread-A").start();
        // 线程 Thread-B:死循环从 queue 队列里面取数据,若取出的数据不为空就打印出来后退出循环
        new Thread(() -> {
            while (true) {
                Reference<? extends FinalizeGC> poll = queue.poll();
                if (null != poll) {
                    System.out.println("虚引用被回收了:" + poll);
                    break;
                }
            }
        }, "Thread-B").start();
        System.in.read();
    }
}

在 VM Options 设置最小/最大堆内存,以便我们能看到虚引用回收时的效果

-Xms25M -Xmx25M

然后再运行代码,结果会如下:

finalize method invoke
Exception in thread "Thread-A" java.lang.OutOfMemoryError: Java heap space
  at org.vnjohn.jvm.PhantomReference.lambda$main$0(PhantomReference.java:22)
  at org.vnjohn.jvm.PhantomReference$$Lambda$1/2093631819.run(Unknown Source)
  at java.lang.Thread.run(Thread.java:750)
虚引用被回收了:java.lang.ref.PhantomReference@618c65e9

基于以上结果,简要分析,线程 Thread-A 往集合中塞数据,随着数据越来越多达到堆内存阈值,肯定就会触发 GC;线程 Thread-B 死循环,从队列 queue 中获取数据,若数据不为空,就打印出来

从执行结果来看,当发生 GC,虚引用就会被回收,并会把回收的引用放入到 ReferenceQueue 中.

虚引用的使用与软引用、弱引用的区别还是挺大的,虚引用的特点如下:

  1. 无法通过虚引用来获取对象的真实引用,PhantomReference#get 方法返回的数据是直接返回 null,不管包裹的对象是什么,都是直接返回 null

创建虚引用对象,除了把包装传入的对象,同时还需要传 ReferenceQueue 参数,从名字来看它代表引用队列

  1. 虚引用必须与 ReferenceQueue 一起使用,当 GC 准备回收一个对象时,若发现它还有虚引用,就会 GC 回收之前,将该虚引用加入到与之关联的引用队列 ReferenceQueue 中

虚引用使用场景:NIO 会使用虚引用来管理堆外内存信息

总结

该篇博文介绍了经典四大引用门将:强软弱虚,以及如何在对象被 GC 回收前重新完成一次自我救赎代码演示,强引用:当引用无关联其他引用时,根不可达时,该引用会被回收;软引用:内存不足时触发 GC 才会回收,适合于作缓存;弱引用:无论内存是否足够,只要触发 GC 都会被回收,ThreadLocal、WeakHashMap 经典案例;虚引用:在内存不足产生 GC 时,会将虚引用进行回收,回收的结果会放入到 ReferenceQueue 引用队列中,希望这块的知识能够对你有许些帮助,感谢支持三连!后续会分析如何判断对象是否已死(其实前提就是说明引用计数、根可达算法之间的应用及区别)?

参考文献:《深入理解 Java 虚拟机》周志明著

博文放在 JVM 专栏里,欢迎订阅,会持续更新!

如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

推荐专栏:Spring、MySQL,订阅一波不再迷路

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!

目录
相关文章
|
Kubernetes 数据可视化 安全
枚举探秘:Java中的神奇力量!
枚举探秘:Java中的神奇力量!
|
4月前
|
缓存 算法 Java
Java面试题:深入探究Java内存模型与垃圾回收机制,Java中的引用类型在内存管理和垃圾回收中的作用,Java中的finalize方法及其在垃圾回收中的作用,哪种策略能够提高垃圾回收的效率
Java面试题:深入探究Java内存模型与垃圾回收机制,Java中的引用类型在内存管理和垃圾回收中的作用,Java中的finalize方法及其在垃圾回收中的作用,哪种策略能够提高垃圾回收的效率
41 1
|
1月前
|
存储 Java 程序员
【一步一步了解Java系列】:何为数组,何为引用类型
【一步一步了解Java系列】:何为数组,何为引用类型
23 1
|
23天前
|
存储 Java 编译器
[Java]基本数据类型与引用类型赋值的底层分析
本文详细分析了Java中不同类型引用的存储方式,包括int、Integer、int[]、Integer[]等,并探讨了byte与其他类型间的转换及String的相关特性。文章通过多个示例解释了引用和对象的存储位置,以及字符串常量池的使用。此外,还对比了String和StringBuilder的性能差异,帮助读者深入理解Java内存管理机制。
18 0
|
4月前
|
Java 开发者 UED
Java中的并发编程:解锁多线程的力量
【7月更文挑战第7天】在Java的世界中,掌握并发编程是提升应用性能和响应能力的关键。本文将深入探讨如何在Java中高效地使用多线程,包括创建和管理线程、同步机制、以及避免常见的并发陷阱。我们将一起探索锁、线程池、并发集合等工具,并了解如何通过这些工具来优化程序的性能和稳定性。
|
5月前
|
缓存 Java 开发者
深入理解Java的五种引用类型
深入理解Java的五种引用类型
|
5月前
|
XML Java 数据格式
“MapStruct妙用指南:解锁Java对象映射的强大力量!“ ️
“MapStruct妙用指南:解锁Java对象映射的强大力量!“ ️
263 0
|
5月前
|
存储 缓存 Java
【Java】Java中的引用类型(全面解读)
【Java】Java中的引用类型(全面解读)
50 0
|
5月前
|
Java 程序员
技术日志:揭秘Java编程 —— 抽象类与接口的隐藏力量!
【6月更文挑战第17天】在Java编程中,抽象类和接口如同内功心法,增强代码灵活性和维护性。抽象类`Course`定义共性属性和行为,如显示大纲,子类如`ProgrammingCourse`继承并实现细节。接口`Ratable`提供评分功能,允许不同课程以多态方式实现。通过抽象类和接口,代码组织更有序,系统扩展性更强,犹如武侠高手以平凡招式创出非凡武学。不断学习和探索这些技术,能提升编程技艺,应对复杂挑战。
39 0
|
6月前
|
存储 安全 Java
Java一分钟之Java数据类型概览:基本类型与引用类型
【5月更文挑战第7天】本文概述了Java中的基本和引用数据类型,强调了理解它们对高效编程的重要性。基本类型包括数值、布尔和字符类型,而引用类型涉及类、接口、数组等。注意基本类型的精度损失和溢出问题,以及引用类型的空指针异常和内存泄漏。通过明确类型范围、使用包装类、空值检查和及时释放资源来避免这些问题。代码示例展示了基本类型和引用类型的使用。理解这些核心概念有助于编写更健壮的Java代码。
47 1