编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 1.1String深入理解原理
  • 1.2浮点型数据深入研究
  • 1.3数据装箱和拆箱原理
  • 1.4泛型由来和设计思想
  • 1.5加密和解密设计和原理
  • 2.1面向对象设计思想
  • 2.2抽象类和接口设计
  • 2.3封装和继承设计思想
  • 2.4复用和组合设计思想
  • 2.5对象和引用设计思想
  • 3.1IO流设计思想和原理
  • 3.2为何设计序列化数据
  • 3.3各种拷贝数据比较
  • 3.4高效文件读写的原理
  • 4.1反射性能探索和优化
  • 4.2为何要设计注解思想
  • 4.3动态代理的设计思想
  • 4.4SPI机制设计的思想
  • 4.5异常设计和捕获原理
  • 4.6虚拟机如何处理异常
  • 4.7四种引用设计思想
  • 5.1线程的前世今生探索
  • 5.2线程通信的设计思想
  • 5.3线程监控和Debug设计
  • 5.4线程和JVM之间联系
  • 5.5线程池使用技巧介绍
  • 5.6线程池设计核心原理
  • 5.7线程如何最大优化
  • 6.1多线程并发经典案例
  • 6.2并发安全前世今生
  • 6.3线程安全如何保证
  • 6.4变量的线程安全探索
  • 6.5并发上下文切换原理
  • 6.6理解CAS设计和由来
  • 6.7协程设计思想和原理
  • 6.8事物并发模型解读
  • 6.9并发设计模型研究
  • 6.10并发编程数据一致性
  • 6.11锁问题的定位和修复
  • 6.12多线程如何性能调优
  • 7.1类的加载过程和原理
  • 7.2对象布局设计的原理
  • 7.3双亲委派机制设计思想
  • 7.5代码攻击和安全防护
  • 7.6设计动态生成Java类

4.7四种引用设计思想

目录介绍

  • 01.四种引用介绍
    • 1.1 都是强引用会怎样
    • 1.2 为何需要4种引用
    • 1.3 引用类型有哪些
  • 02.一般使用场景
    • 2.1 强引用的场景
    • 2.2 软引用场景分析
    • 2.3 弱引用场景分析
    • 2.4 虚引用场景未遇到
    • 2.5 四种引用比较
  • 03.四种引用演示
    • 3.1 强引用演示
    • 3.2 软引用演示
    • 3.3 弱引用演示
    • 3.4 虚引用演示
  • 04.四种引用设计思想
    • 4.1 JVM设计引用维度
    • 4.2 指针&引用&句柄
    • 4.3 引用和引用队列设计
    • 4.4 引用队列使用模版
    • 4.5 工具类Cleaner使用
  • 05.四种引用源码分析
    • 5.1 Reference源码设计
    • 5.2 SoftReference源码分析
    • 5.3 WeakReference源码分析
    • 5.4 PhantomReference源码分析
  • 06.引用队列设计原理
    • 6.1 ReferenceQueue设计
    • 6.2 引用对象和实际对象关联
    • 6.3 引用对象入队过程

01.四种引用介绍

1.1 都是强引用会怎样

  • 如果所有的引用都是强引用,那么对象的生命周期将完全由程序控制。
    • 对象只有在没有任何强引用指向它时,才会被垃圾回收器回收。这意味着只有当没有任何引用指向一个对象时,垃圾回收器才会释放该对象所占用的内存。
    • 只有在显式地将引用设置为null或超出引用的作用域时,对象才会被释放。
  • 如果都是强引用,会引起那些问题?
    • 内存泄漏:如果程序中存在不再使用的对象,但仍然有强引用指向它们,这些对象将无法被垃圾回收器回收,从而导致内存泄漏。
    • 内存占用过高:由于没有及时释放不再使用的对象,内存占用可能会持续增长,导致应用程序占用过多的内存资源。影响应用程序的性能和响应性,并且可能导致系统资源的浪费。
    • 对象生命周期管理困难:由于对象的生命周期完全由程序控制,需要开发人员自行管理对象的创建、使用和销毁。增加代码复杂性。

1.2 为何需要4种引用

  • Java设计者为何要设计四种不同的引用呢?
    • 提供更灵活的内存管理和对象生命周期控制。使得开发人员可以更灵活地管理对象的生命周期和内存使用。
    • 通过使用软引用、弱引用和虚引用,可以实现一些高级的内存管理技术,例如缓存、对象池和资源清理等。
    • 这些引用类型的选择取决于具体的需求和场景,开发人员可以根据实际情况选择合适的引用类型来管理对象的生命周期。
  • java.lang.ref包中提供了几个类:
    • SoftReference类、WeakReference类和PhantomReference类,它们分别代表软引用、弱引用和虚引用。
    • ReferenceQueue类表示引用队列,它可以和这三种引用类联合使用,以便跟踪Java虚拟机回收所引用的对象的活动。

1.3 引用类型有哪些

  • 引用类型有哪些种
    • 强引用:默认的引用方式,不会被垃圾回收,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
    • 软引用(SoftReference):如果一个对象只被软引用指向,只有内存空间(堆内存)不足够时,垃圾回收器才会回收它;
    • 弱引用(WeakReference):如果一个对象只被弱引用指向,当JVM进行垃圾回收时,无论内存是否充足,都会回收该对象。
    • 虚引用(PhantomReference):虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用通常和ReferenceQueue配合使用。

02.一般使用场景

2.1 强引用的场景

  • 关于强引用引用的场景,直接new出来的对象
    • String str = new String("yc");
    • 通过引用,可以对堆中的对象进行操作。在某个函数中,当创建了一个对象,该对象被分配在堆中,通过这个对象的引用才能对这个对象进行操作。
  • 强引用的特点
    • 强引用可以直接访问目标对象。强引用可能导致内存泄露。注意相互引用情况。
  • 如何回收强引用
    • 如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。
    • 看看Vector类的清理方法:在清除数据的时候,将数组中的每个元素都置为null,中断强引用与对象之间的关系,让GC的时候能够回收这些对象的内存。
    protected Object[] elementData;
    
    public synchronized void removeAllElements() {
        modCount++;
        // Let gc do its work
        for (int i = 0; i < elementCount; i++)
            elementData[i] = null;
    
        elementCount = 0;
    }
  • 思考:将强引用设置成null后在什么时候jvm回收对象呢?

2.2 软引用场景

  • 软引用应用场景
    • 例如从网络上获取图片,然后将获取的图片显示的同时,通过软引用缓存起来。当下次再去网络上获取图片时,首先会检查要获取的图片缓存中是否存在,若存在,直接取出来,不需要再去网络上获取。
  • 软引用的简单使用用法如下
    MyObject aRef = new  MyObject();
    SoftReference aSoftRef = new SoftReference(aRef);
    MyObject anotherRef = (MyObject)aSoftRef.get();
  • 软引用的特点
    • 如果一个对象只具有软引用,那么如果内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
    • 软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
  • 实际应用案例
    • 正常是用来处理图片这种占用内存大的情况,这样使用软引用好处
    • 通过软引用的get()方法,取得drawable对象实例的强引用,发现对象被未回收。在GC在内存充足的情况下,不会回收软引用对象。此时view的背景显示
    • 实际情况中,我们会获取很多图片,然后可能给很多个view展示, 这种情况下很容易内存吃紧导致oom,内存吃紧,系统开始会GC。这次GC后,drawables.get()不再返回Drawable对象,而是返回null,这时屏幕上背景图不显示,说明在系统内存紧张的情况下,软引用被回收。
    • 使用软引用以后,在OutOfMemory异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免Crash发生。
  • 注意避免软引用获取对象为null
    • 在垃圾回收器对这个Java对象回收前,SoftReference类所提供的get方法会返回Java对象的强引用,一旦垃圾线程回收该Java对象之后,get方法将返回null。
    • 所以在获取软引用对象的代码中,一定要判断是否为null,以免出现NullPointerException异常导致应用崩溃。

2.3 弱引用场景

  • 关于WeakReference弱引用特点
    • 如果一个对象只具有弱引用,那么在垃圾回收器线程扫描的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
    • 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
    • 弱引用也可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
  • 防止内存泄漏,要保证内存被虚拟机回收
    • 为什么handler会造成内存泄漏?这种情况就是由于android的特殊机制造成的:当一个android主线程被创建的时候,同时会有一个Looper对象被创建,而这个Looper对象会实现一个MessageQueue(消息队列),当我们创建一个handler对象时,而handler的作用就是放入和取出消息从这个消息队列中,每当我们通过handler将一个msg放入消息队列时,这个msg就会持有一个handler对象的引用。
    • 因此当Activity被结束后,这个msg在被取出来之前,这msg会继续存活,但是这个msg持有handler的引用,而handler在Activity中创建,会持有Activity的引用,因而当Activity结束后,Activity对象并不能够被gc回收,因而出现内存泄漏。
  • 根本原因
    • Activity在被结束之后,MessageQueue并不会随之被结束,如果这个消息队列中存在msg,则导致持有handler的引用,但是又由于Activity被结束了,msg无法被处理,从而导致永久持有handler对象。
    • handler永久持有Activity对象,于是发生内存泄漏。但是为什么为static类型就会解决这个问题呢?因为在java中所有非静态的对象都会持有当前类的强引用,而静态对象则只会持有当前类的弱引用。
    • 声明为静态后,handler将会持有一个Activity的弱引用,而弱引用会很容易被gc回收,这样就能解决Activity结束后,gc却无法回收的情况。当然解决源头还是在Activity退出的时候,移除Handler内部消息队列的数据。
  • 解决办法,采用弱引用管理handler,代码如下所示
    private MyHandler handler = new MyHandler(this);
    private static class MyHandler extends Handler{
        WeakReference<FirstActivity> weakReference;
        MyHandler(FirstActivity activity) {
            weakReference = new WeakReference<>(activity);
        }
    
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
            }
        }
    }

2.4 虚引用场景

  • 关于PhantomReference类虚引用
    • 虚引用是所有引用类型中最弱的一个。一个持有虚引用的对象,和没有引用几乎是一样的,随时都可能被垃圾回收器回收。
    • 当试图通过虚引用的get()方法取得强引用时,总是会失败。并且,虚引用必须和引用队列一起使用,它的作用在于跟踪垃圾回收过程。
    • 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收后,销毁这个对象,奖这个虚引用加入引用队列。
  • Android实际开发中没有用到过
    • 貌似开发中没有接触过虚引用

2.5 四种引用比较

  • 弱引用和软引用区别
    • 只具有弱引用的对象拥有更短暂的生命周期,可能随时被回收。而只具有软引用的对象只有当内存不够的时候才被回收,在内存足够的时候,通常不被回收。
  • 使用软引用或者弱引用防止内存泄漏
    • 在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。
    • 软引用,弱引用都非常适合来保存那些可有可无的缓存数据。如果这样做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间。
  • 到底什么时候使用软引用,什么时候使用弱引用呢?
    • 个人认为,如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。
    • 还有就是可以根据对象是否经常使用来判断。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。
    • 另外,和弱引用功能类似的是WeakHashMap。WeakHashMap对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的回收,回收以后,其条目从映射中有效地移除。WeakHashMap使用ReferenceQueue实现的这种机制。

03.四种引用演示

3.1 强引用演示

  • Java中默认声明的就是强引用,比如:
    User user = new User(); // 创建一个User对象,使用强引用
    System.gc(); // 手动触发GC
    if(user == null){
        System.out.println("user对象已经被GC回收    " + user);
    }else {
        System.out.println("user对象没有被GC回收    " + user);
    }
    user = null; // 手动置null 取消引用
  • 只要强引用存在,垃圾回收器将永远不会回收被引用的对象
    • 哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。
    • 如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象。
  • 思考:将强引用设置成null后在什么时候jvm回收对象呢?
    • 当一个对象的所有强引用都被设置为null后,该对象变为不可达,垃圾回收器在运行时会检测并回收该对象。
    • 具体的回收时机是由垃圾回收器自行决定的,可能会受到多种因素的影响,如垃圾回收策略、内存压力等。
  • 垃圾回收器的具体行为和时机是由Java虚拟机(JVM)的垃圾回收策略决定的,而不是由程序中的null赋值触发的。
    • 垃圾回收器的工作是自动的,它会周期性地检测不可达对象并回收它们所占用的内存。
  • 具体来说,当垃圾回收器运行时,它会进行以下步骤:
    • 标记阶段:垃圾回收器会从根对象(如栈帧中的局部变量、静态变量等)开始,递归地遍历对象图,并标记所有可达的对象。
    • 清除阶段:垃圾回收器会清除所有未被标记的对象,即不可达对象。这些对象所占用的内存将被释放。
    • 压缩或整理阶段(可选):某些垃圾回收器可能会对内存进行压缩或整理,以减少内存碎片和提高内存利用率。

3.2 软引用演示

  • 软引用可用来实现内存敏感的高速缓存。要模拟出这个效果需要在启动参数中设置堆内存大小:-Xmx1M -Xms1M
    • 如何模拟系统内存不足?设置堆内存大小,然后创建一个达到阀值大小的对象,这样就可以模拟内存不足。
    SoftReference<User> softRef = new SoftReference(new User()); // 软引用
    System.gc(); // 手动触发GC
    if(softRef.get() == null){
        System.out.println("user对象已经被GC回收    " + softRef.get());
    }else {
        System.out.println("user对象没有被GC回收    " + softRef.get());
    }
    // 模拟内存溢出 这里创建1M的Byte数组
    try {
        Byte[] bytes = new Byte[1024 * 1024 * 1024];
    }catch (OutOfMemoryError e){
        e.printStackTrace();
    }
    // 查看软引用是否被回收
    if(softRef.get() == null){
        System.out.println("user对象已经被GC回收    " + softRef.get());
    }else {
        System.out.println("user对象没有被GC回收    " + softRef.get());
    }

3.3 弱引用演示

  • 只有弱引用的对象,当JVM触发gc时,就会回收该对象。
    WeakReference<User> weakRef = new WeakReference(new User()); // 弱引用
    if(weakRef.get() == null){
        System.out.println("user对象已经被GC回收    " + weakRef.get());
    }else {
        System.out.println("user对象没有被GC回收    " + weakRef.get());
    }
    System.gc(); // 手动触发GC
    if(weakRef.get() == null){
        System.out.println("user对象已经被GC回收    " + weakRef.get());
    }else {
        System.out.println("user对象没有被GC回收    " + weakRef.get());
    }

3.4 虚引用演示

  • 虚引用主要用来跟踪对象被垃圾回收器回收的活动。
    User user = new User(); //创建一个强引用对象
    ReferenceQueue referenceQueue = new ReferenceQueue(); // 引用队列
    PhantomReference<User> phantomReference = new PhantomReference<>(user, referenceQueue); // 使用虚引用指向这个内存空间 并且绑定引用队列
    System.out.println(phantomReference.get()); // 虚引用是获取不到引用值的 get方法返回的null
    user = null; // 释放这个内存空间,此时只剩phantomReference通过虚引用指向它
    System.gc(); // 手动触发GC
    // 被清除的队列中取出被回收的对象
    while (true) {
        Reference<? extends User> poll = referenceQueue.poll();
        if (poll!=null) {
            System.out.println("虚引用对象被回收 处理其它逻辑");
             return;
        }
    }

04.四种引用设计思想

4.1 JVM设计引用维度

  • Java 引用是 Java 虚拟机为了实现更加灵活的对象生命周期管理而设计的对象包装类,一共有四种引用类型。将它们的区别概括为 3 个维度:
  • 维度 1 - 对象可达性状态的区别:
    • 强引用指向的对象是强可达的,而其他引用指向的对象都是弱可达的。当一个对象存在到 GC Root 的引用链时,该对象被认为是强可达的。
    • 只有强可达的对象才会认为是存活的对象,才能保证在垃圾收集的过程中不会被回收;
  • 维度 2 - 垃圾回收策略的区别: 除了影响对象的可达性状态,不同的引用类型还会影响垃圾收集器回收对象的激进程度:
    • 强引用: 强引用指向的对象不会被垃圾收集器回收;
    • 软引用: 软引用是相对于强引用更激进的策略,软引用指向的对象在内存充足时会从垃圾收集器中豁免,起到类似强引用的效果,但在内存不足时还是会被垃圾收集器回收。那么软引用通常是用于实现内存敏感的缓存,当有足够空闲内存时保留内存,当空闲内存不足时清理缓存,避免缓存耗尽内存;
    • 弱引用和虚引用: 弱引用和虚引用是相对于软引用更激进的策略,弱引用指向的对象无论在内存是否充足的时候,都会被垃圾收集器回收;
  • 维度 3 - 感知垃圾回收时机:
    • 虚引用主要的作用是提供了一个感知对象被垃圾回收的机制。在虚拟机即将回收对象之前,如果发现对象还存在虚引用,则会在回收对象后会将引用加入到关联的引用队列中。
    • 程序可以通过观察引用队列的方式,来感知到对象即将被垃圾回收的时机,再采取必要的措施。例如 Java Cleaner 工具类,就是基于虚引用实现的回收工具类。
    • 需要特别说明的是,并不是只有虚引用才能与引用队列关联,软引用和弱引用都可以与引用队列关联,只是说虚引用唯一的作用就是感知对象垃圾回收时机。

4.2 指针&引用&句柄

  • 引用、指针和句柄都具有指向对象地址的含义,可以将它们都简单地理解为一个内存地址。只有在具体的问题中,才需要区分它们的含义:
    • 1、引用(Reference): 引用是 Java 虚拟机为了实现灵活的对象生命周期管理而实现的对象包装类,引用本身并不持有对象数据,而是通过直接指针或句柄 2 种方式来访问真正的对象数据;
    • 2、指针(Point): 指针也叫直接指针,它表示对象数据在内存中的地址,通过指针就可以直接访问对象数据;
    • 3、句柄(Handler): 句柄是一种特殊的指针,句柄持有指向对象实例数据和类型数据的指针。使用句柄的优点是让对象在垃圾收集的过程中移动存储区域的话,虚拟机只需要改变句柄中的指针,而引用持有的句柄是稳定的。缺点是需要两次指针访问才能访问到对象数据。
  • 直接指针访问
    • image
      image
  • 句柄访问
    • image
      image

4.3 引用和引用队列设计

  • 引用为何需要设计引用队列,设计引用队列的意图是什么?
    • 引用队列(Reference Queue)是一种特殊的数据结构,用于跟踪被垃圾回收器回收的对象。
    • 引用队列的设计主要是为了提供一种机制,允许程序在对象被回收时进行一些额外的处理或通知。它在监控对象的回收、资源释放和清理发挥重要作用!
  • 引用队列的设计有以下几个主要原因:
    • 监控对象的回收:引用队列允许程序监控对象的回收情况。当一个对象被垃圾回收器回收时,它会被放入与之关联的引用队列中。通过检查引用队列,程序可以得知对象是否已被回收,从而进行相应的处理。
    • 资源释放和清理:引用队列可以用于释放和清理对象所占用的资源。当对象被回收时,程序可以从引用队列中获取到相应的引用,并执行一些清理操作,如关闭文件、释放网络连接等。
    • 弱引用和软引用的使用:引用队列通常与弱引用(Weak Reference)和软引用(Soft Reference)一起使用。通过将弱引用或软引用与引用队列关联,程序可以在对象被回收时得到通知,并进行相应的处理。
  • 使用引用对象
    • 1、创建引用对象: 直接通过构造器创建引用对象,并且直接在构造器中传递关联的实际对象和引用队列。引用队列可以为空,但虚引用必须关联引用队列,否则没有意义;
    • 2、获取实际对象: 在实际对象被垃圾收集器回收之前,通过 Reference#get() 可以获取实际对象,在实际对象被回收之后 get() 将返回 null,而虚引用调用 get() 方法永远是返回 null;
    • 3、解除关联关系: 调用 Reference#clear() 可以提前解除关联关系。

4.4 引用队列使用模版

  • 以下为 ReferenceQueue 的使用模板,主要分为 2 个阶段:
    • 阶段 1: 创建引用队列实例,并在创建引用对象时关联该队列;
    • 阶段 2: 对象在被垃圾回收后,引用对象会被加入引用队列(根据下文源码分析,引用对象在进入引用队列的时候,实际对象已经被回收了)。通过观察 ReferenceQueue#poll() 的返回值可以感知对象垃圾回收的时机。
    // 阶段 1:
    // 创建对象
    String strongRef = new String("abc");
    // 1、创建引用队列
    ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
    // 2、创建引用对象,并关联引用队列
    WeakReference<String> weakRef = new WeakReference<>(strongRef, referenceQueue);
    System.out.println("Reference-> weakRef 1:" + weakRef);
    // 3、断开强引用
    strongRef = null;
    System.gc();
    // 阶段 2:
    // 延时 5000 是为了提高 "abc" 被回收的概率
    new Handler().postDelayed(() -> {
      System.out.println("Reference-> " + weakRef.get()); // 输出 null
      // 观察引用队列
      Reference<? extends String> ref = referenceQueue.poll();
      System.out.println("Reference-> weakRef 2:" + ref);
      if (null != ref) {
          // 虽然可以获取到 Reference 对象,但无法获取到引用原本指向的对象
          System.out.println("Reference-> " + ref.get()); // 输出 null
      }
    }, 5000);
  • 程序输出。ReferenceQueue 中大部分 API 是面向 Java 虚拟机内部的,只有 ReferenceQueue#poll() 是面向开发者的。它是非阻塞 API,在队列有数据时返回队头的数据,而在队列为空时直接返回 null。
    Reference-> weakRef 1:java.lang.ref.WeakReference@75bed0d
    Reference-> abc
    Reference-> weakRef 2:null

4.5 工具类Cleaner使用

  • 在ReferenceQueue类中,那么enqueueLocked(Reference)函数中的Cleaner是做什么的。在stackoverflow网站中找到这个解释
    • sun.misc.Cleaner是JDK内部提供的用来释放非堆内存资源的API。JVM只会帮我们自动释放堆内存资源,但是它提供了回调机制,通过这个类能方便的释放系统的其他资源。
    • 可以看到Cleaner是用于释放非堆内存的,所以做特殊处理。
    • 通过enqueue和isEnqueue两个函数的分析,ReferenceQueue队列维护了那些被回收对象referent的Reference的引用,这样通过isEnqueue就可以判断对象referent是否已经被回收,用于一些情况的处理。
  • Cleaner 是虚引用的工具类,用于实现在对象被垃圾回收时额外执行一段清理逻辑,本质上只是将虚引用和引用队列等代码做了简单封装而已。
    import sun.misc.Cleaner;
    
    // 1、创建对象
    String strongRef = new String("abc");
    // 2、创建清理逻辑
    CleanerThunk thunk = new CleanerThunk();
    // 3、创建 Cleaner 对象(本质上是一个虚引用)
    Cleaner cleaner = Cleaner.create(strongRef, thunk);
    
    private class CleanerThunk implements Runnable {
        @Override
        public void run() {
            // 清理逻辑
        }
    }

05.四种引用源码分析

5.1 Reference源码设计

  • Reference设计成抽象类,源码如下所示
    • 通过get()方法获取对象,通过clear()方法清除对象。这两个方法最终都调用native,里面究竟是如何实现的呢?
    // 引用对象公共父类
    public abstract class Reference<T> {
    
        // 虚拟机内部使用
        volatile T referent;
    
        // 关联引用队列
        final ReferenceQueue<? super T> queue;
      
        Reference(T referent) {
            this(referent, null);
        }
    
        Reference(T referent, ReferenceQueue<? super T> queue) {
            this.referent = referent;
            this.queue = queue;
        }
    
        // 获取引用指向的实际对象
        public T get() {
            // 调用 Native 方法
            return getReferent();
        }
    
        @FastNative
        private final native T getReferent();
    
        // 解除引用与实际对象的关联关系
        public void clear() {
            // 调用 Native 方法
            clearReferent();
        }
    
        @FastNative
        native void clearReferent();
        ...
    }

5.2 SoftReference源码分析

  • SoftReference源码设计如下
    public class SoftReference<T> extends Reference<T> {
    
        public SoftReference(T referent) {
            super(referent);
        }
    
        public SoftReference(T referent, ReferenceQueue<? super T> q) {
            super(referent, q);
        }
    }
  • 关于这段源码分析
    • 可以看到SoftReference有一个类变量clock和一个变量timestamp,这两个参数对于SoftReference至关重要。
      • clock:记录了上一次GC的时间。这个变量由GC(garbage collector)来改变。
      • timestamp:记录对象被访问(get函数)时最近一次GC的时间。
    • 那么这两个参数有什么用?
      • 我们知道软引用是当内存不足时可以回收的。但是这只是大致情况,实际上软应用的回收有一个条件:
      • clock - timestamp <= free_heap * ms_per_mb
      • free_heap是JVM Heap的空闲大小,单位是MB
      • ms_per_mb单位是毫秒,是每MB空闲允许保留软引用的时间。Sun JVM可以通过参数-XX:SoftRefLRUPolicyMSPerMB进行设置
    • 举个栗子:
      • 目前有3MB的空闲,ms_per_mb为1000,这时如果clock和timestamp分别为5000和2000,那么
      • 5000 - 2000 <= 3 * 1000
      • 条件成立,则该次GC不对该软引用进行回收。
      • 所以每次GC时,通过上面的条件去判断软应用是否可以回收并进行回收,即我们通常说的内存不足时被回收。

5.3 WeakReference源码分析

  • WeakReference源码设计如下
    public class WeakReference<T> extends Reference<T> {
    
        public WeakReference(T referent) {
            super(referent);
        }
    
        public WeakReference(T referent, ReferenceQueue<? super T> q) {
            super(referent, q);
        }
    }

5.4 PhantomReference源码分析

  • PhantomReference源码设计如下
    public class PhantomReference<T> extends Reference<T> {
    
        // 虚引用 get() 永远返回 null
        public T get() {
            return null;
        }
    
        // 虚引用必须管理引用队列,否则没有意义
        public PhantomReference(T referent, ReferenceQueue<? super T> q) {
            super(referent, q);
        }
    }
  • 源码分析说明
    • 可以看到get函数返回null,正如前面说得虚引用无法获取对象引用。(注意网上有些文章说虚引用不持有对象的引用,这是有误的,通过构造函数可以看到虚引用是持有对象引用的,但是无法获取该引用
    • 同时可以看到虚引用只有一个构造函数,所以必须传入ReferenceQueue对象。
    • 前面提到虚引用的作用是判断对象是否被回收,这个功能正是通过ReferenceQueue实现的。
    • 这里注意:不仅仅是虚引用可以判断回收,弱引用和软引用同样实现了带有ReferenceQueue的构造函数,如果创建时传入了一个ReferenceQueue对象,同样也可以判断。

06.引用队列设计原理

6.1 ReferenceQueue设计

  • 如果让你来设计引用队列,你会如何设计?
    • 队列,可以想到排队进站。先排队的人在前面,前面的人按照队列顺序逐个进站。这个是先进进出的次序!
  • ReferenceQueue 是基于单链表实现的队列
    • 元素按照先进先出的顺序出队(Java OpenJDK 和 Android 中的 ReferenceQueue 实现略有区别,OpenJDK 以先进后出的顺序出队,而 Android 以先进先出的顺序出队)。
  • 然后看一下ReferenceQueue源码设计
    • image
      image
    public abstract class Reference<T> {
        // 关联的引用队列
        final ReferenceQueue<? super T> queue;
        // 单链表后继指针
        Reference queueNext;
    }
    
    public class ReferenceQueue<T> {
        // 入队	
        boolean enqueue(Reference<? extends T> reference) {
            synchronized (lock) {
                if (enqueueLocked(reference)) {
                    lock.notifyAll();
                    return true;
                }
                return false;
            }
        }
    
        // 出队
        public Reference<? extends T> poll() {
            synchronized (lock) {
                if (head == null)
                    return null;
    
                return reallyPollLocked();
            }
        }
    
        // 入队
        private boolean enqueueLocked(Reference<? extends T> r) {
            // 处理 Cleaner 逻辑
            if (r instanceof Cleaner) {
                Cleaner cl = (sun.misc.Cleaner) r;
                cl.clean();
                r.queueNext = sQueueNextUnenqueued;
                return true;
            }
            // 尾插法
            if (tail == null) {
                head = r;
            } else {
                tail.queueNext = r;
            }
            tail = r;
            tail.queueNext = r;
            return true;
        }
          
        // 出队
        private Reference<? extends T> reallyPollLocked() {
            if (head != null) {
                Reference<? extends T> r = head;
                if (head == tail) {
                    tail = null;
                    head = null;
                } else {
                    head = head.queueNext;
                }
                r.queueNext = sQueueNextUnenqueued;
                return r;
            }
            return null;
        }
    }

6.2 引用对象和实际对象关联

  • Reference#get() 和 Reference#clear() 可以获取或解除关联关系,它们是在 Native 层实现的。
    • 具体看:java_lang_ref_Reference.cc
    namespace art {
    
    // 对应 Java native 方法 Reference#getReferent() 
    static jobject Reference_getReferent(JNIEnv* env, jobject javaThis) {
        ScopedFastNativeObjectAccess soa(env);
        ObjPtr<mirror::Reference> ref = soa.Decode<mirror::Reference>(javaThis);
        ObjPtr<mirror::Object> const referent = Runtime::Current()->GetHeap()->GetReferenceProcessor()->GetReferent(soa.Self(), ref);
        return soa.AddLocalReference<jobject>(referent);
    }
    
    // 对应 Java native 方法 Reference#clearReferent()
    static void Reference_clearReferent(JNIEnv* env, jobject javaThis) {
        ScopedFastNativeObjectAccess soa(env);
        ObjPtr<mirror::Reference> ref = soa.Decode<mirror::Reference>(javaThis);
        Runtime::Current()->GetHeap()->GetReferenceProcessor()->ClearReferent(ref);
    }
    
    // 动态注册 JNI 函数
    static JNINativeMethod gMethods[] = {
        FAST_NATIVE_METHOD(Reference, getReferent, "()Ljava/lang/Object;"),
        FAST_NATIVE_METHOD(Reference, clearReferent, "()V"),
    };
    
    void register_java_lang_ref_Reference(JNIEnv* env) {
        REGISTER_NATIVE_METHODS("java/lang/ref/Reference");
    }
    
    }  // namespace art

6.3 引用对象入队过程

  • 引用对象加入引用队列的过程发生在垃圾收集器的处理过程中,我将相关流程概括为 2 个阶段:

  • 阶段 1: 在垃圾收集的标记阶段,垃圾收集器会标记在本次垃圾收集中豁免的对象(包括强引用对象、FinalizerReference 对象以及不需要在本次回收的 SoftReference 软引用对象)。

    • 当一个引用对象指向的实际对象没有被标记时,说明该对象除了被引用对象引用之外已经不存在其他引用关系。
    • 那么垃圾收集器会解除引用对象与实际对象的关联关系,并且将引用对象暂存到一个全局链表 unenqueued 中,随后 notify 正在等待类对象的线程
    • image
      image
    // 临时的全局链表
    public static Reference<?> unenqueued = null;
    
    // 从 Native 层调用
    static void add(Reference<?> list) {
     synchronized (ReferenceQueue.class) {
         // 此处使用尾插法将 list 加入全局链表 unenqueued,代码略
         // 唤醒等待类锁的线程
         ReferenceQueue.class.notifyAll();
     }
    }
  • 那么,谁在等待这个类对象呢?

    • 其实,在虚拟机启动时,会启动一系列守护线程,其中就包括处理引用入队的 ReferenceQueueDaemon 线程和 Finalizer 机制的 FinalizerDaemon 线程,这里唤醒的正是ReferenceQueueDaemon 线程。
    //https://www.androidos.net.cn/android/9.0.0_r8/xref/art/runtime/runtime.cc
    void Runtime::StartDaemonThreads() {
        // 调用 java.lang.Daemons.start()
        Thread* self = Thread::Current();
        JNIEnv* env = self->GetJniEnv();
        env->CallStaticVoidMethod(WellKnownClasses::java_lang_Daemons, WellKnownClasses::java_lang_Daemons_start);
    }
    
    //https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/libart/src/main/java/java/lang/Daemons.java
    public static void start() {
        // 启动四个守护线程:
        // ReferenceQueueDaemon:处理引用入队
        ReferenceQueueDaemon.INSTANCE.start();
        // FinalizerDaemon:处理 Finalizer 机制
        FinalizerDaemon.INSTANCE.start();
        FinalizerWatchdogDaemon.INSTANCE.start();
        HeapTaskDaemon.INSTANCE.start();
    }
  • 阶段 2: ReferenceQueueDaemon 线程会使用等待唤醒机制轮询消费这个全局链表 unenqueued,如果链表不为空则将引用对象投递到对应的引用队列中,否则线程会进入等待。

    private static class ReferenceQueueDaemon extends Daemon {
        private static final ReferenceQueueDaemon INSTANCE = new ReferenceQueueDaemon();
    
        ReferenceQueueDaemon() {
            super("ReferenceQueueDaemon");
        }
    
        // 阶段 2:轮询 unenqueued 全局链表
        @Override public void runInternal() {
            while (isRunning()) {
                Reference<?> list;
                // 2.1 同步块
                synchronized (ReferenceQueue.class) {
                    // 2.2 检查 unenqueued 全局链表是否为空
                    while (ReferenceQueue.unenqueued == null) {
                        // 2.3 为空则等待 ReferenceQueue.class 类锁
                        ReferenceQueue.class.wait();
                    }
                    list = ReferenceQueue.unenqueued;
                    ReferenceQueue.unenqueued = null;
                }
                // 2.4 投递引用对象
                // 为什么放在同步块之外:因为 list 已经从静态变量 unenqueued 剥离处理,不用担心其他线程会插入新的引用,所以可以放在 synchronized{} 块之外
                ReferenceQueue.enqueuePending(list);
            }
        }
    }
    
    private static class FinalizerDaemon extends Daemon {
        ...
    }
    
    // 2.4 投递引用对象
    public static void enqueuePending(Reference<?> list) {
        Reference<?> start = list;
        do {
            ReferenceQueue queue = list.queue;
            if (queue == null) {
                // 2.4.1 没有关联的引用队列,则不需要投递
                Reference<?> next = list.pendingNext;
                list.pendingNext = list;
                list = next;
            } else {
                // 2.4.2 为了避免反复加锁,这里选择一次性投递相同引用队列的对象
                synchronized (queue.lock) {
                    do {
                        Reference<?> next = list.pendingNext;
                        list.pendingNext = list;
                        // 2.4.3 引用对象入队
                        queue.enqueueLocked(list);
                        list = next;
                    } while (list != start && list.queue == queue);
                    // 2.4.4 唤醒 queue.lock,跟 remove(...) 有关
                    queue.lock.notifyAll();
                }
            }
        } while (list != start);
    }
  • 至此,引用对象已经加入 ReferenceQueue 中的双向链表,等待消费者调用 ReferenceQueue#poll() 消费引用对象。

    • image
      image
  • 强引用、软引用、弱引用、幻象引用有什么区别?

  • https://time.geekbang.org/column/article/6970

  • https://www.cnblogs.com/pengxurui/p/16576791.html

贡献者: yangchong211
上一篇
4.6虚拟机如何处理异常
下一篇
5.1线程的前世今生探索