Java ~ Reference ~ Cleaner【总结】

news2024/12/28 3:59:10

前言


 文章

  • 相关系列:《Java ~ Reference【目录】》(持续更新)
  • 相关系列:《Java ~ Reference ~ Cleaner【源码】》(学习过程/多有漏误/仅作参考/不再更新)
  • 相关系列:《Java ~ Reference ~ Cleaner【总结】》(学习总结/最新最准/持续更新)
  • 相关系列:《Java ~ Reference ~ Cleaner【问题】》(学习解答/持续更新)

一 概述


 简介

    需要提前说明的是,本文讲述的是JDK8中位于sun.misc包下的Cleaner(清洁工)类(高版本中被迁移至jdk.internal.ref包)。而从JDK9开始,Java在java.lang.ref包下实现了另一个清洁工类,且该类并非PhantomReference(虚引用)类的子类。虽说两者作用高度相似(应该是对旧功能的新实现),但实际上两者都被保留下来并各自发挥着作用,因此请注意区别两者。由于新清洁工类自身携带有一套完整运行流程的缘故,因此目前的主流说法中都将JDK9版本的清洁工类的运行流程称之为清洁工机制,故本文中不会出现清洁工机制这个称呼。

    清洁工类是专门为了替代Finalization(终结)机制/finalize()方法而实现的,学习者很容易冒出这个想法…该想法是否正确暂且不论,但清洁工类确实实现了与终结机制/finalize()方法相同的功能,即在所指对象被GC回收时执行自定义操作。与常规注册了引用队列的Reference(引用)抽象类对象不同,拿WeakReference(弱引用)类对象举例,如果我们想在其所指对象被GC回收时执行一些操作,首先需要等待引用机制将弱引用置入引用队列中,随后再将之从中取出后或执行弱引用的isEnqueued()方法,因为我们需要通过该操作来判断所指对象是否已/会被GC回收。换句话说就是我们需要先手动的判断所指对象是否已/会被GC回收再去执行自定义操作…这就增加了我们编码的复杂性,但如果使用清洁工的话我们就不需要再做这一步了。

    清洁工类继承自虚引用类,这意味着其本身也是一个虚引用。当清洁工的所指对象被GC回收时,按照引用机制的统一流程,其会被置入引用队列中。但之前在引用抽象类的文章中已经特意提及过,引用机制在将引用加入引用队列前存在一个特殊判断,即如果引用机制发现引用是一个清洁工,则会执行其内部包含的自定义操作。这意味着我们无需再做手动的判断,甚至于自定义操作都不会发生在用户线程中,引用机制会直接在后台自动处理执行自定义逻辑,从而简化了开发者编码。

在这里插入图片描述

    事实上上述做法是不规范的,虽然这看起来只是单纯的特殊判断,但本质却是上级耦合了下级,在父类中对子类做特殊处理并不合常规…因此会有清洁工类是专门为了替代终结机制/finalize()方法而实现的这种想法也就理所当然了。除此之外还有一点也能应征这个想法,即清洁工在执行完自定义操作后会放弃加入引用队列,并直接退出流程开始对下个引用做处理…连引用队列都不入了…说它不是专门为了取代终结机制/finalize()方法谁信呐?

 与终结机制/finalize()方法的对比

    不推荐使用清洁工。说了这么多,结果不让用,感觉有点浪费感情…但是实际上我们确实不推荐使用清洁工,因为其效果会好一些,但基本上终结机制/finalize()方法有的问题它都有。如果真的要说好处,大概就是性能好了些(终结机制/finalize()方法的执行建立在完整的引用机制流程基础上,而清洁工类则直接从中截断,性能自然会好些)和处理速度快了些(引用机制的引用处理器线程为最高优先级10,大于终结机制/finalize()方法的执行线程8的优先级,因此能获得更多的CPU资源),但总体来说还是不推荐使用的…程序哪天崩了都不知道。

二 使用


 创建

    清洁工类没有公共的构造方法,具体的实例通过其提供的工厂方法创建,该设计与清洁工的生命周期有关。

  • public static Cleaner create(Object var0, Runnable var1) —— 创建 —— 创建指定所指对象及自定义操作的清洁工,该清洁工会被默认以头插法加入清洁工链表中。
        方法首先会判断传入的自定义操作是否为null,是则直接返回null,因为一个没有指定自定义操作的清洁工是没有意义的;否则调用私有Cleaner(Object var1, Runnable var2)构造方法创建指定所指对象及自定义操作的清洁工。清洁工作为虚引用理论上必须搭配引用队列使用,但由于清洁工会收到引用机制的特殊处理,即其并不会被加入引用队列中,因此在创建清洁工时无需手动指定引用队列,清洁工类会使用自带的全局静态常量“假”引用队列来默认填充虚引用类的构造方法参数。
        清洁工被成功创建后,方法会自动以头插法将之加入清洁工链表中,这也是清洁工类只提供工厂方法而不提供公共构造方法的根本原因,其目的是确保每个被创建的清洁工都默认存在于清洁工链表里。清洁工链表的作用是保证清洁工无论在什么环境下被使用,都不会在自定义操作被引用机制执行前被GC回收,该知识点会在下文详述。
public static Cleaner create(Object var0, Runnable var1) {
    // 实例化一个清洁工,并将之插入到清洁工链表中。
    return var1 == null ? null : add(new Cleaner(var0, var1));
}

private Cleaner(Object var1, Runnable var2) {
    // 调用虚引用类的构造方法。清洁工的本质是一个虚引用类对象,因此必然存在一个所指对象。
    super(var1, dummyQueue);
    // 设置自定义操作。
    this.thunk = var2;
}

 方法

  • public void clean() —— 清理 —— 将当前清洁工从清洁工链表中移除并执行自定义操作。该方法只在首次调用时有效,即只会在首次调用时执行自定义操作,后续调用不会再执行。该方法会在引用机制中被调用。

三 实现


 清洁工链表

    清洁工链表的核心作用是防止清洁工在被引用机制执行自定义操作前被GC回收。我们并无法限定清洁工在实际使用中的场景,也就是说无论开发者是有意还是无意,清洁工都可能处于一个完全没有与GC ROOTS建立直接/间接关联的场景中,如此造成的后果就是清洁工完全可能在被引用机制执行自定义操作前被GC回收(更准确的说是加入待定列表前,因为待定链表也是全局静态变量,属于GC ROOTS的范畴,但发现列表不是)。这个后果是不可以被接受的,因为清洁工并不像纯粹的软/弱/虚引用一样只是单纯的影响所指对象的GC,其本身携带的自定义操作也注定了它属于所指对象运行逻辑的一部分,因此为了保证清洁工的自定义操作一定会被执行,就必须人为将清洁工与GC ROOTS建立直接/间接关联,而清洁工链表起的就是这个作用。

    清洁工链表是由清洁工类内部持有的全局唯一双向链表,即清洁工链表是一个全局静态变量。清洁工链表是逻辑链表,即清洁工链表并不是一个类似于LinkedList(链接列表)类对象的对象。清洁工类中用于持有清洁工链表的[first @ 首个]实际上持有的只是清洁工链表的头清洁工/节点,由于每个清洁工都持有其前驱/后继清洁工/节点,因此只要持有了头清洁工/节点就相当于持有了整个清洁工链表。

    当清洁工被创建时,其会被默认以头插法加入清洁工链表中,这也是清洁工类不提供公共构造方法的根本原因之一,而另一个根本原因是避免创建未指定自定义操作的清洁工。由于每个清洁工在创建时就会被默认加入清洁工链表中,因此所有清洁工与GC ROOTS之间都至少会存在一条可达链路,故而无论清洁工在具体的使用场景中是否与GC ROOTS建立了其它直接/间接关联,都保证了其在被引用机制执行自定义操作之前不会被GC回收。而当执行自定义操作时,即引用机制调用清洁工的clean()方法时,clean()方法会先将清洁工从清洁工链表中移除后再执行自定义操作,目的是断开额外可达链路以防止在自定义操作执行后受其影响而无法被GC回收(虽然额外链路被断开,但此时清洁工还被作为引用处理器线程的局部变量,因此无需担心自定义操作执行期间清洁工被GC回收)。

    clean()方法只有在首次调用时有效。关于这一点在上文中已经提及过,由于clean()方法是公共方法,因此其也可能被开发者调用,在开发者已手动调用clean()方法的情况下,引用机制调用clean()方法也不会再重复执行自定义操作(反之亦然),这是因为将清洁工从清洁工链表移除是执行自定义操作的先决条件,而一个清洁工无法被移除多次(但凭心而论自定义操作只能被执行一次应该是设计上的问题,否则完全可以将清洁工移除和自定义操作执行拆分为两步操作,或者无论是否移除都可自由执行)。由于所有清洁工会共用同一个清洁工链表,因此清洁工类的插入/移除静态方法被修饰了synchronized关键字实现类锁以保证线程安全。清洁工的移除无需遍历清洁工链表,因为清洁工本身持有前驱/后继清洁工/节点,因此可以直接进行链接。当然,如果是头清洁工/节点则需要特殊判断以更新[首个]。清洁工被移除后会将之前驱/后继清洁工/节点设置为自身,即自引用,作为其已被移除的判断依据,clean()方法就是通过该依据判断清洁工是否已被移除/方法是否已被执行的(之所以不使用null是因为null已经被作为头/尾清洁工/节点的标志)。

在这里插入图片描述

四 使用案例


    清洁工类有一个较为著名的使用案例,即堆外内存的回收,而要阐述清楚这一点,就必须了解DirectByteBuffer(直接比特缓存)类。直接比特缓存类与堆外内存的管理有关,在此不做讨论,只需知道其用于分配/回收堆外内存即可。直接比特缓存虽然只是普通的Java对象,却可能关联着一块非常大的堆外内存,因此直接比特缓存被GC回收前必须释放/回收这些堆外内存,否则这些堆外内存会一直被Java进程持有而无法重新分配,因为众所周知,GC在正常情况下是无法释放/回收堆外内存的(也有不正常的情况,当堆内存溢出的时会触发Full GC顺便一起回收)。

    为了保证堆外内存一定被释放,直接比特缓存类使用清洁工类实现了一个保底机制,即当直接比特缓存被GC回收时,会触发清洁工对堆外内存进行回收。在直接比特缓存类中组合了一个清洁工类字段,该字段会在直接比特缓存初始化时被赋值为清洁工,该清洁工以当前直接比特缓存作为所指对象,源码如下:

private final Cleaner cleaner;

// Primary constructor
//
DirectByteBuffer(int cap) {                   // package-private
    // 此处忽略。
    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    // 创建清洁工。
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

protected DirectByteBuffer(int cap, long addr, FileDescriptor fd, Runnable unmapper) {
    super(-1, 0, cap, cap, fd);
    address = addr;
    // 创建清洁工。
    cleaner = Cleaner.create(this, unmapper);
    att = null;
}

    在上述代码中,创建清洁工时第二个参数传入了一个Deallocator(释放者)类对象。释放者类是直接比特缓存类自实现的实现了Runnable(可运行)接口的静态内部类,其在run()方法中编码了堆外内存的释放逻辑。源码如下:

/**
 * @Description: 释放者/器类(看不懂没关系,知道是释放堆外内存的就行)
 */
private static class Deallocator implements Runnable {

    private static Unsafe unsafe = Unsafe.getUnsafe();

    private long address;
    private long size;
    private int capacity;

    private Deallocator(long address, long size, int capacity) {
        assert (address != 0);
        this.address = address;
        this.size = size;
        this.capacity = capacity;
    }
    
    /**
     * @Description: 释放堆外内存
     */
    public void run() {
        if (address == 0) {
            // Paranoia
            return;
        }
        // 释放对外内存。
        unsafe.freeMemory(address);
        address = 0;
        Bits.unreserveMemory(size, capacity);
    }

}

    当直接比特缓存被GC回收时,清洁工也会因为引用而执行入队操作,从而引发对清洁工的特殊处理。清洁工不会入队,而是会执行其内部的自定义操作,也就是释放者的run()方法,从而完成堆外内存的释放。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/635765.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[java]关于Session关于Token关于JWT

目录 关于Session 关于Token 关于JWT 关于Session HTTP协议是一种无状态协议,即:当某个客户端向服务器发起请求,服务器端进行处理,后续,此客户端再次发起请求,服务器端并不能直接知道它就是此前来访过的…

chatgpt赋能python:Python怎么多行输入?教你高效开发!

Python怎么多行输入?教你高效开发! 1. 介绍 Python是一种可读性高、可编程性强、拥有丰富的第三方模块和库的高级编程语言。作为典型的解释型语言,它可在多个平台上进行开发和运行,凭借其优雅、简洁、高效的语法风格和强大的功能…

Linux下C/C++ 多线程SSH扫描与暴力攻击

Secure Shell(安全外壳协议,简称SSH)是一种加密的网络传输协议,可在不安全的网络中为网络服务提供安全的传输环境。SSH通过在网络中建立安全隧道来实现SSH客户端与服务器之间的连接。 为什么需要SSH 如果没有SSH,绝大…

chatgpt赋能python:Python怎么安装skimage?

Python怎么安装skimage? 如果你之前使用Python编程,你可能会遇到需要安装第三方库的情况。对于图像处理任务,你可能需要用到scikit-image(也称为skimage)这个库。本文将提供一个详细的指南来安装skimage。 1. 确保你…

【JVM篇】类加载过程详解

目录 1、类加载过程概述 2、加载 3、连接 3.1 验证 3.1.1 文件格式验证 3.1.2 元数据验证 3.1.3 字节码验证 3.1.4 符号引用验证 3.2 准备 3.3 解析 4、初始化 1、类加载过程概述 想必大家一般在网上看类加载过程的资料时,通常资料只会将类加载过程概括…

OMG--DDS(Data Distribution Service)

OMG--DDS(Data Distribution Service) 1 介绍1.1 概述1.2 OMG 涉及的规范 2 内容概述介绍目标 Data-Centric Publish-Subscribe (DCPS) 以数据为中心的发布-订阅概要Platform Independent Model (PIM) 平台独立模型格式和约定概念图总体概念模型PIM 描述…

ChatGPT工作提效之数据可视化大屏组件Echarts的实战方案(大数据量加载、伪3D饼图、地图各省cp中心坐标属性、map3D材质)

ChatGPT工作提效系列文章目录 ChatGPT工作提效之初探路径独孤九剑遇强则强ChatGPT工作提效之在程序开发中的巧劲和指令(创建MySQL语句、PHP语句、Javascript用法、python的交互)ChatGPT工作提效之生成开发需求和报价单并转为Excel格式ChatGPT工作提效之小鹅通二次开发批量API对…

【电路】电路与电子技术基础 课堂笔记 第7章 晶体管放大电路

7.1 放大的概念 7.1.1 放大电路基础 放大电路可以将电信号不失真地进行放大,而且是幅度放大; 本质上,放大是对能量进行控制和转换, 由一个能量较小的输入信号控制直流电源, 将直流电源的能量转换成与输入信号频率…

yolov8量化部署(基于openvino和tensorrt)

yolov8 openvino量化部署 环境配置: pip install ultralytics && pip install openvino-dev将pytorch模型转为openvino模型: from ultralytics import YOLO# Load a model model YOLO("./yolov8n.pt") # load an official model# Export the…

S7-200 PLC编程软件介绍

更多关于西门子S7-200PLC内容请查看:西门子200系列PLC学习课程大纲(课程筹备中) 西门子200PLC编程软件采用的是STEP 7-Micro/WIN 软件。它可以进行编写程序,PLC程序下载与上传,编程向导,程序编译,PLC程序监控等等功能…

软件工程开发文档写作教程(12)—概要设计书的编制目标

本文原创作者:谷哥的小弟作者博客地址:http://blog.csdn.net/lfdfhl本文参考资料:电子工业出版社《软件文档写作教程》 马平,黄冬梅编著 概要设计书概述 《概要设计说明书》又称为《系统设计说明书》,编制的目的是说明…

Cracking C++(10): 基本的输入输出流

文章目录 1. 目的2. I/O Streams 输入/输出流3. Stream Operators 流操作符4. 禁止使用 std::endl5. 缓冲区:直观理解6. References 1. 目的 查看 hackingcpp 上的 Input & Output (Basics) 教程后的笔记和拓展内容。 2. I/O Streams 输入/输出流 使用 C 的标…

chatgpt赋能python:合并多个文件——Python的终极解决方案

合并多个文件——Python的终极解决方案 Python是一种高级编程语言,其简单明了的语法和丰富的库使其成为开发者的首选语言之一。在日常编码中,我们有时候需要将多个文件合并成一个文件以便于处理。这篇文章将详细介绍Python如何进行多个文件的合并。 什…

高数下——查漏补缺

期末复习 一、向量与空间几何 二、多元函数与重极限 2.1定义域 2.2 二元函数重极限

【实战】体验SadTalker

论文http://openaccess.thecvf.com//content/CVPR2023/papers/Zhang_SadTalker_Learning_Realistic_3D_Motion_Coefficients_for_Stylized_Audio-Driven_Single_CVPR_2023_paper.pdf github GitHub - OpenTalker/SadTalker: [CVPR 2023] SadTalker:Learning Realist…

chatgpt赋能python:Python如何合并单元格-实用技巧教程

Python如何合并单元格 - 实用技巧教程 单元格合并是Excel文件和Word文档等办公软件中很常见的功能,而使用Python对数据进行处理时,也可能需要实现合并单元格的操作。本文将为大家介绍Python中实现合并单元格的方法,并给出代码示例。 什么是…

HTML段落标签

HTML段落标签 段落标签 显示特点: 代码: 显示效果: 总结: 答案:

Java性能权威指南-总结10

Java性能权威指南-总结10 垃圾收集算法理解G1垃圾收集器 垃圾收集算法 理解G1垃圾收集器 G1垃圾收集器是一种工作在堆内不同分区上的并发收集器。分区(region)既可以归属于老年代,也可以归属于新生代(默认情况下,一个堆被划分成2048个分区),同一个代的…

chatgpt赋能python:如何取消Python中的科学计数法

如何取消Python中的科学计数法 Python是一种面向对象、解释型的高级编程语言。由于它在数值计算、科学计算以及数据分析等领域的强大功能,Python语言已经成为科学计算和机器学习领域中最常用的语言之一。 然而,当我们进行大量运算时,Python…

[SpringBoot]Spring Security框架

目录 关于Spring Security框架 Spring Security框架的依赖项 Spring Security框架的典型特征 关于Spring Security的配置 关于默认的登录页 关于请求的授权访问(访问控制) 使用自定义的账号登录 使用数据库中的账号登录 关于密码编码器 使用BCry…