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

这篇具有很好参考价值的文章主要介绍了深入理解 Java 引用类型:强壮、柔软、脆弱、虚无的力量。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

🔭 大家好,我是 vnjohn,在互联网企业担任 Java 开发,CSDN 优质创作者
📖 推荐专栏:Spring、MySQL、Nacos、Java,后续其他专栏会持续优化更新迭代
🌲文章所在专栏:JVM
🤔 我当前正在学习微服务领域、云原生领域、消息中间件等架构、原理知识
💬 向我询问任何您想要的东西,ID:vnjohn
🔥觉得博主文章写的还 OK,能够帮助到您的,感谢三连支持博客🙏
😄 代词: vnjohn
⚡ 有趣的事实:音乐、跑步、电影、游戏

目录

前言

引用是用于访问对象的变量,它是内存中的一个指针,指向堆中的对象实例;通过 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 弱引用

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

在 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

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

在 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,订阅一波不再迷路

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!文章来源地址https://www.toymoban.com/news/detail-514948.html

到了这里,关于深入理解 Java 引用类型:强壮、柔软、脆弱、虚无的力量的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Java 四大引用类型

    GC 回收的优先级上 : 强引用 - 软引用 - 弱引用 - 虚引用 当内存不足, JVM 开始进行 gc 时,对于强引用的对象不会进行回收,即使出现了 OOM 也不会回收。因此,强引用是造成内存泄露的主要原因。 当进行 GC 时,如果内存还足够,则不进行回收该对象,若内存不够,则回收该

    2024年02月11日
    浏览(41)
  • Java中的各种引用类型以及部分引用的相关例子

    在Java中,引用类型主要有四种,分别是:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。这些类型通常与垃圾回收机制有关,用来描述对象的生命周期和可达性。下面详细介绍每一种引用类型: 强引用 (Strong Reference) 强

    2024年04月13日
    浏览(41)
  • java 中都有哪些引用类型?

    Java中 默认声明的就是强引用 ,比如: 只要 强引用存在,垃圾回收器将永远不会回收被引用的对象 。如果 想被回收 ,可以将对象置为 null ; 在内存足够的时候,软引用不会被回收,只有在内存不足时,系统才会回收软引用对象,如果回收了软引用对象之后仍然没有足够的

    2023年04月26日
    浏览(56)
  • 探索 DTD 在 XML 中的作用及解析:深入理解文档类型定义

    DTD 是文档类型定义(Document Type Definition)的缩写。DTD 定义了 XML 文档的结构以及合法的元素和属性。 通过使用 DTD,独立的团体可以就数据交换的标准 DTD 达成一致。 应用程序可以使用 DTD 来验证 XML 数据的有效性。 如果 DTD 在 XML 文件内声明,它必须包裹在 DOCTYPE 定义内:

    2024年04月26日
    浏览(34)
  • Java中的四种引用类型及其使用方式

    Java中有四种引用类型,分别是强引用(Strong Reference)、软引用(Soft Reference)、弱引用(WeakReference)、虚引用(PhantomReference)。 这要从Java管理内存的方式说起。Java为了将程序员从内存管理中解救出来,即不让程序员自己申请堆内存(比如C语言程序员需要通过malloc请求操作系统分配一

    2023年04月09日
    浏览(48)
  • Java基础知识——类、静态、继承、引用类型使用

    类的定义格式如下: 例如: 例如: 1.3.1 封装的步骤 1.使用 private 来修饰成员变量。 2.使用 public 修饰getter和setter方法。 1.3.2 封装的步骤实现 private修饰成员变量 public修饰getter和setter方法 1.4.1 构造器的作用 通过调用构造器可以返回一个类的对象,构造器同时负责帮我们把

    2023年04月19日
    浏览(45)
  • [Java]关于基本数据类型与引用类型赋值时的底层分析的小结(简述)

    【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) https://www.cnblogs.com/cnb-yuchen/p/17969159 出自【进步*于辰的博客】 目录 1、关于赋值 1.1 基本数据类型赋值 1.2 String类型赋值 2、关于String赋值 2.1 情形一 2.2 情形二 3、关于String与char[]的比较 4、不同类型引

    2024年01月17日
    浏览(40)
  • 美团一面:说一说Java中的四种引用类型?

    在JDK1.2之前Java并没有提供软引用、弱引用和虚引用这些高级的引用类型。而是提供了一种基本的引用类型,称为 Reference 。并且当时Java中的对象只有两种状态:被引用和未被引用。当一个对象被引用时,它将一直存在于内存中,直到它不再被任何引用指向时,才会被垃圾回收

    2024年03月22日
    浏览(42)
  • Android Java代码与JNI交互 引用类型转换(五)

    🔥 Android Studio 版本 🔥   🔥 Java中基础数据类型对应Native的数据类型 🔥 🔥 Java中引用数据类型 对应Native中的数据类型 🔥   🔥 创建  JNIReferenceType 🔥 🔥 生成可关联的库链接 🔥 为了让Java能够调用reference-type-lib库中的函数,您需要使用 CMake 构建脚本中的 target_link_l

    2024年02月13日
    浏览(45)
  • 深入理解HTTP请求中常见的数据类型包括表单数据、JSON、XML和文件上传等。

    本文将介绍HTTP请求中常见的数据类型,包括表单数据、JSON、XML和文件上传等。通过详细解释每种数据类型的特点、用途和示例,帮助读者深入理解并正确使用这些常见的HTTP数据类型。 在Web开发中,HTTP是一种常用的通信协议,用于客户端和服务器之间的数据交互。HTTP请求中

    2024年02月10日
    浏览(86)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包