Java中自动清理资源的方式(虚引用的作用)

前言

Java是没有析构函数的,所以在一个类的生命周期结束时,它约等于猝死,谁知道啥时候没有人挂念了,系统又要gc了,然后就消失了,什么善后也做不了。但是有许多类都需要显式关闭资源,包括一些本地方法库,甚至JDK自带的很多类都需要(比如OutputStream)。

关闭资源除了人尽皆知的finalize方法,还有更优雅的try-with-resources、虚引用+cleaner方式。本文主要介绍后两种方式,因此也可以说明虚引用的作用。

先说说finalize()方法

记得在孤尽老师的课上听Object的8种方法如何记忆的时候,就提到finalize解决了对象的“我要到哪里去”的问题——跑题了

finalize方法从jdk9开始已经标记为废弃了,原因是(下面一段搬砖自jdk11的finalize方法的说明)

The finalization mechanism is inherently problematic. Finalization can lead to performance issues, deadlocks, and hangs. Errors in finalizers can lead to resource leaks; there is no way to cancel finalization if it is no longer necessary; and no ordering is specified among calls to finalize methods of different objects. Furthermore, there are no guarantees regarding the timing of finalization. The finalize method might be called on a finalizable object only after an indefinite delay, if at all. Classes whose instances hold non-heap resources should provide a method to enable explicit release of those resources, and they should also implement AutoCloseable if appropriate. The ref.Cleaner and ref.PhantomReference provide more flexible and efficient ways to release resources when an object becomes unreachable.

简而言之,它有问题(比如无法保证一定执行、何时执行、按什么顺序执行等等),java引入了更优雅的方式进行资源释放。主要有以下几种方式:

  • 实现AutoCloseable接口(try-with-resource)
  • 使用ref.Cleaner和ref.PhantomReference机制

那么,我们就展开说这两种方式。

自动清理资源的两种方式

实现AutoCloseable接口(try-with-resource)

jdk1.7引入了了try-with-resource的用法,大致如下(太懒了直接搬运这篇博文里的代码了)

public class TryWithResource {
  public static void main(String[] args) {
    try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
       BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
      int b;
      while ((b = bin.read()) != -1) {
        bout.write(b);
      }
    }
    catch (IOException e) {
      e.printStackTrace();
    }
  }
}

简而言之,在语法使用层面上,就是try后面跟一个括号,括号里声明并赋值一个需要显式关闭的资源。相比于之前的try-finally来说,更加优雅。(python也有类似用法,就是with xxx:)

那所有的类都能支持try-with-resource吗?No!但是难度也不大,只要实现了AutoCloseable接口就可以。这里直接搬运该接口的定义和官方说明:

/**
 * An object that may hold resources (such as file or socket handles)
 * until it is closed. The {@link #close()} method of an {@code AutoCloseable}
 * object is called automatically when exiting a {@code
 * try}-with-resources block for which the object has been declared in
 * the resource specification header. This construction ensures prompt
 * release, avoiding resource exhaustion exceptions and errors that
 * may otherwise occur.
 */
public interface AutoCloseable {
    void close() throws Exception;
}

简而言之,实现了这个接口,就能用try-with-resources语法。可以看到这个接口内部只有一个方法,就是close()。所以我们也可以想像一下,语法的实现大致就是和try-finally语句块一样,最后调用一下类的close()方法即可。一般都会有catch语句块,所以出现异常时也可以捕获并处理。

使用ref.Cleaner和ref.PhantomReference机制

从这里开始,就要说到虚引用的用法了。

之前在看深入理解JVM的时候,看到了java的四种引用,分别是强引用、软引用、弱引用、虚引用,前三种引用都说得很清楚,唯有虚引用,作者没有说清是干什么的,原文如下:

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

那么什么叫“回收时收到一个系统通知”呢?懵逼。

后来看到一篇讲直接内存的文章时,里面说到了直接内存的回收方式,在这里才明白了虚引用的一种用法:替代finalize()方法,用于资源的释放和回收。那么本文也结合直接内存中虚引用的使用方法,争取把虚引用的清理功能给它说透说明白。

简而言之

如果要用非常简单的话来概括这一机制,那大概就是:

对于虚引用来说,当其不存在其它引用时,会被gc标记上。有一个高优先级的线程“java.ref.Reference.ReferenceHandler”会处理这些被标记的虚引用(实际上可能不止虚引用,但是虚引用会被这个线程处理),将其加入设定好的引用队列中,也就是所述的“得到一个通知”

Cleaner继承了虚引用,在ReferenceHandler中会对Cleaner对象执行短路逻辑,直接执行Cleaner接口的clean()方法而不会入队。

这里展现一下这个会创建一个线程ReferenceHandler的执行逻辑:

public abstract class Reference {
    // 该类的静态语句块内,会创建一个线程ReferenceHandler,不停地(在死循环内)执行该方法
    private static void processPendingReferences() {
        // 等待VM给出标记过的引用,会阻塞
        waitForReferencePendingList();
        Reference<Object> pendingList;
        synchronized (processPendingLock) {
            pendingList = getAndClearReferencePendingList();
            processPendingActive = true;
        }
        while (pendingList != null) {
            Reference<Object> ref = pendingList;
            pendingList = ref.discovered;
            ref.discovered = null;
            
            // 重点在这里!!!如果待清理的引用是一个Cleaner对象,直接调用它的clean()方法进行清理
            if (ref instanceof Cleaner) {
                synchronized (processPendingLock) {
                    processPendingLock.notifyAll();
                }
            } 
            // 否则,将这个Reference<Object>对象加入引用队列,以待使用者自己做善后工作
            else {
                ReferenceQueue<? super Object> q = ref.queue;
                if (q != ReferenceQueue.NULL) q.enqueue(ref);
            }
        }

        synchronized (processPendingLock) {
            processPendingActive = false;
            processPendingLock.notifyAll();
        }
    }
}

例子:直接内存如何使用Cleaner+虚引用

之前说了Cleaner+虚引用的用法是看一篇关于直接内存的文章才知道的,所以这里就举这个例子。

还是简要的描述一下这个过程:

DirectBuffer对象本身存储在堆内存中,但是会关联一大片堆外内存;

由于堆外内存不会被GC自动回收,因此DB对象创建时会关联一个Cleaner对象;

当DirectBuffer不再使用,它的虚引用,也就是Cleaner对象,会被ThreadHandler处理(处理逻辑刚刚已经展示过了),它的clean()方法会回收直接内存。

有了刚刚的铺垫,用法应该很快就看明白了。最后贴几段相关的代码说明一下、

// DirectBuffer的创建过程,重点看最下面
class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer {
    DirectByteBuffer(int cap) {

        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)) {
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        // directbuffer创建时,会关联一个Cleaner对象,并且绑定一个清理函数
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }
}

关于Cleaner:

// 看看Cleaner对象的创建&清理过程
// 首先要看到:它是继承了PhantomReference的
public class Cleaner extends PhantomReference<Object> {
    // 创建一个Cleaner对象。ob是其关联对象,thunk是清理逻辑
    public static Cleaner create(Object ob, Runnable thunk) {
        if (thunk == null)
            return null;
        // 这里会将Cleaner对象插入自己的链表中,避免关联对象还未死亡,自己就已经被gc了
        return add(new Cleaner(ob, thunk));
    }
    
    public void clean() {
        // 让自己出队,这样以后自己就可以被gc清理了
        if (!remove(this))
            return;
        try {
            // 执行清理逻辑
            thunk.run();
        } catch (final Throwable x) {
            AccessController.doPrivileged(new PrivilegedAction<>() {
                    public Void run() {
                        if (System.err != null)
                            new Error("Cleaner terminated abnormally", x)
                                .printStackTrace();
                        System.exit(1);
                        return null;
                    }});
        }
    }
}

再看一眼PhantomReference的说明文档:

Phantom reference objects, which are enqueued after the collector determines that their referents may otherwise be reclaimed. Phantom references are most often used for scheduling pre-mortem cleanup actions in a more flexible way than is possible with the Java finalization mechanism.
翻译过来就是,它比finalization机制更灵活,或者说是用来替代finalization的吧!

后记

本来只是想写关于虚引用的用法的,但是在写的过程中,由于看到了Object.finalize()函数的deprecated说明,就把题目改成了java资源的自动清理方式,说明我不是简单的搬运(大概变成组合和大杂烩了)

孤尽老师曾经说过,学习有四个阶段,记忆、理解、表达、融会贯通,其中表达最难。写文章就等于是表达的过程,也算是形成了学习的“闭环”。虽然经常大家会拿闭环来开玩笑,但是闭环真的很重要,在闭环的过程中,如果对某个环节理解得不够深入,论证和表述就缺少说服力;如果没有形成完整的环,只是个半圆或者差一点点成环,那就很难理解事情背后的抽象、做这件事的原因,只是纠结在知识点上,管中窥豹而已。

热门文章

暂无图片
编程学习 ·

那些年让我们目瞪口呆的bug

程序员一生与bug奋战&#xff0c;可谓是杀敌无数&#xff0c;见怪不怪了&#xff01;在某知识社交平台中&#xff0c;一个“有哪些让程序员目瞪口呆的bug”的话题引来了6700多万的阅读&#xff0c;可见程序员们对一个话题的敏感度有多高。 1、麻省理工“只能发500英里的邮件” …
暂无图片
编程学习 ·

redis的下载与安装

下载redis wget http://download.redis.io/releases/redis-5.0.0.tar.gz解压redis tar -zxvf redis-5.0.0.tar.gz编译 make安装 make install快链方便进入redis ln -s redis-5.0.0 redis
暂无图片
编程学习 ·

《大话数据结构》第三章学习笔记--线性表(一)

线性表的定义 线性表&#xff1a;零个或多个数据元素的有限序列。 线性表元素的个数n定义为线性表的长度。n为0时&#xff0c;为空表。 在比较复杂的线性表中&#xff0c;一个数据元素可以由若干个数据项组成。 线性表的存储结构 顺序存储结构 可以用C语言中的一维数组来…
暂无图片
编程学习 ·

对象的扩展

文章目录对象的扩展属性的简洁表示法属性名表达式方法的name属性属性的可枚举性和遍历可枚举性属性的遍历super关键字对象的扩展运算符解构赋值扩展运算符AggregateError错误对象对象的扩展 属性的简洁表示法 const foo bar; const baz {foo}; baz // {foo: "bar"…
暂无图片
编程学习 ·

让程序员最头疼的5种编程语言

世界上的编程语言&#xff0c;按照其应用领域&#xff0c;可以粗略地分成三类。 有的语言是多面手&#xff0c;在很多不同的领域都能派上用场。大家学过的编程语言很多都属于这一类&#xff0c;比如说 C&#xff0c;Java&#xff0c; Python。 有的语言专注于某一特定的领域&…
暂无图片
编程学习 ·

写论文注意事项

参考链接 给研究生修改了一篇论文后&#xff0c;该985博导几近崩溃…… 重点分析 摘要与结论几乎重合 这一条是我见过研究生论文中最常出现的事情&#xff0c;很多情况下&#xff0c;他们论文中摘要部分与结论部分重复率超过70%。对于摘要而言&#xff0c;首先要用一小句话引…
暂无图片
编程学习 ·

安卓 串口开发

上图&#xff1a; 上码&#xff1a; 在APP grable添加 // 串口 需要配合在项目build.gradle中的repositories添加 maven {url "https://jitpack.io" }implementation com.github.licheedev.Android-SerialPort-API:serialport:1.0.1implementation com.jakewhart…
暂无图片
编程学习 ·

2021-2027年中国铪市场调研与发展趋势分析报告

2021-2027年中国铪市场调研与发展趋势分析报告 本报告研究中国市场铪的生产、消费及进出口情况&#xff0c;重点关注在中国市场扮演重要角色的全球及本土铪生产商&#xff0c;呈现这些厂商在中国市场的铪销量、收入、价格、毛利率、市场份额等关键指标。此外&#xff0c;针对…
暂无图片
编程学习 ·

Aggressive cows题目翻译

描述&#xff1a; Farmer John has built a new long barn, with N (2 < N < 100,000) stalls.&#xff08;John农民已经新建了一个长畜棚带有N&#xff08;2<N<100000&#xff09;个牛棚&#xff09; The stalls are located along a straight line at positions…
暂无图片
编程学习 ·

剖析组建PMO的6个大坑︱PMO深度实践

随着事业环境因素的不断纷繁演进&#xff0c;项目时代正在悄悄来临。设立项目经理转岗、要求PMP等项目管理证书已是基操&#xff0c;越来越多的组织开始组建PMO团队&#xff0c;大有曾经公司纷纷建造中台的气质&#xff08;当然两者的本质并不相同&#xff0c;只是说明这个趋势…
暂无图片
编程学习 ·

Flowable入门系列文章118 - 进程实例 07

1、获取流程实例的变量 GET运行时/进程实例/ {processInstanceId} /变量/ {变量名} 表1.获取流程实例的变量 - URL参数 参数需要值描述processInstanceId是串将流程实例的id添加到变量中。变量名是串要获取的变量的名称。 表2.获取流程实例的变量 - 响应代码 响应码描述200指…
暂无图片
编程学习 ·

微信每天自动给女[男]朋友发早安和土味情话

微信通知&#xff0c;每天给女朋友发早安、情话、诗句、天气信息等~ 前言 之前逛GitHub的时候发现了一个自动签到的小工具&#xff0c;b站、掘金等都可以&#xff0c;我看了下源码发现也是很简洁&#xff0c;也尝试用了一下&#xff0c;配置也都很简单&#xff0c;主要是他有一…
暂无图片
编程学习 ·

C语言二分查找详解

二分查找是一种知名度很高的查找算法&#xff0c;在对有序数列进行查找时效率远高于传统的顺序查找。 下面这张动图对比了二者的效率差距。 二分查找的基本思想就是通过把目标数和当前数列的中间数进行比较&#xff0c;从而确定目标数是在中间数的左边还是右边&#xff0c;将查…
暂无图片
编程学习 ·

项目经理,你有什么优势吗?

大侠被一个问题问住了&#xff1a;你和别人比&#xff0c;你的优势是什么呢? 大侠听到这个问题后&#xff0c;脱口而出道&#xff1a;“项目管理能力和经验啊。” 听者抬头看了一下大侠&#xff0c;显然听者对大侠的这个回答不是很满意&#xff0c;但也没有继续追问。 大侠回家…
暂无图片
编程学习 ·

nginx的负载均衡和故障转移

#注&#xff1a;proxy_temp_path和proxy_cache_path指定的路径必须在同一分区 proxy_temp_path /data0/proxy_temp_dir; #设置Web缓存区名称为cache_one&#xff0c;内存缓存空间大小为200MB&#xff0c;1天没有被访问的内容自动清除&#xff0c;硬盘缓存空间大小为30GB。 pro…
暂无图片
编程学习 ·

业务逻辑漏洞

身份认证安全 绕过身份认证的几种方法 暴力破解 测试方法∶在没有验证码限制或者一次验证码可以多次使用的地方&#xff0c;可以分为以下几种情况︰ (1)爆破用户名。当输入的用户名不存在时&#xff0c;会显示请输入正确用户名&#xff0c;或者用户名不存在 (2)已知用户名。…