Java编程语言爱好者禅与计算机程序设计艺术

java虚引用

2023-01-10  本文已影响0人  pq217

前言

之前一篇文章java对象的四种引用方式介绍了java对象的四种引用方式:强、软、弱、虚,并重点关照了弱引用的使用场景和其在ThreadLocal源码中发挥的作用

本文则重点介绍一下另外一种引用:虚引用的使用场景和案例

虚引用

虚引用应该可以说是最弱的一种java对象引用方式,其它的引用方式至少还能get到对象,而虚引用的句柄是获取不到对象的,正如它的名字一样:形同虚设

使用

// 新建一个对象
User obj = new User();
// 存储被回收的对象
ReferenceQueue<User> QUEUE = new ReferenceQueue<>();
// phantomReference使用虚引用指向这个内存空间
PhantomReference<User> phantomReference = new PhantomReference<>(obj, QUEUE);

到此我们的句柄phantomReference就通过虚引用指向了新建的User对象,当我们尝试通过句柄获取这个对象时,是取不到的:

System.out.println(phantomReference.get()); // 获取不到 打印为null

作用

那么这个虚引用连获取都获取不到,有啥用哪?创建虚引用时传入的队列QUEUE又有什么用呐?

虚引用的作用就是在对象被GC回收时能得到通知

如何通知呐?就是在对象被回收后,把它的弱引用对象(PhantomReference)存入QUEUE对列中,这样我们查看队列就可以得知某个对象被GC回收了

static class User {}

public static void main(String[] args) {
    // 新建一个对象,开辟一个内存空间
    User obj = new User();
    // 存储被回收的对象的虚引用对象
    ReferenceQueue<User> QUEUE = new ReferenceQueue<>();
    // phantomReference使用虚引用指向这个内存空间
    PhantomReference<User> phantomReference = new PhantomReference<>(obj, QUEUE);
    // 释放这个内存空间,此时只剩phantomReference通过虚引用指向它
    obj = null;
    // 调用gc回收new User的内存空间
    System.gc();
    // 被清除的队列中取出被回收的对象
    while (true) {
        Reference<? extends User> poll = QUEUE.poll();
        if (poll!=null) {
            System.out.println("--obj is recovery--");
        }
    }
}

最终输出“--obj is recovery--”,即我们得到了对象被GC的消息

说白了,虚引用的存在意义就是监控对象是否存活

场景

什么场景下我们可以考虑使用虚引用呐?

比如我们某个对象映射到代码外的一个实际资源,那么一般我们在创建对象时会同时创建这个资源,当然希望在对象被销毁前释放或删除资源,由于手动删除总会可能出现忘记的情况,这时我们就希望在对象销毁时得到通知,如果忘记手动销毁则自动销毁

mysql驱动使用虚引用

上面说的可能相对抽象,以mysql为例,mysql驱动当创建了Connection对象时,会对应生成一个客户端和数据库服务的真实存在的网络连接,当查询完数据后,可以通过connection.close()方法关闭这个连接,但是。。。如果忘了写这句关闭的代码难道就永远保持连接吗?那必然是不行的,所以mysql驱动使用了虚引用,在Connection对象被垃圾回收时,自动执行关闭网络连接的方法

DirectByteBuffer使用虚引用

还有一个经典的例子,就是直接内存DirectByteBuffer,当然这个没mysql好理解,可以看前文Buffer/ByteBuffer/ByteBuf详解了解一下

简单的说,DirectByteBuffer的对象对应的空间是堆栈之外的非jvm内存空间,一个DirectByteBuffer对象对应一处堆外空间,堆外空间不归GC管控,所以当DirectByteBuffer对象被销毁时,当然这部分堆外空间应该被释放

DirectByteBuffer也是使用虚引用的方式,在对象被销毁后释放堆外内存空间

写个例子

接下来自己模拟一个场景,使用虚引用来解决问题

如下场景:我们的每创建一个User对象对应在数据库中生成一条数据,当对象销毁时删除这条数据

用户类代码如下

/**
* 用户类
*/
static class User {
    public DatabaseClient databaseClient;
    public User() {
        // 初始化客户端
        databaseClient = new DatabaseClient();
        // 创建时数据库创建数据
        this.databaseClient.create();
    }
}
/**
* 数据库客户端
*/
static class DatabaseClient {
    /**
     * 创建用户数据
     */
    public void create() {
        System.out.println("--数据库创建用户数据--");
    }
    /**
     * 删除用户数据
     */
    public void remove() {
        System.out.println("--数据库删除用户数据--");
    }
}

此时如果运行如下代码

public static void main(String[] args) {
    User obj = new User();
    // 释放这个内存空间
    obj = null;
    // 调用gc
    System.gc();
}

输出:--数据库创建用户数据--

说明知道代码运行结束,user被回收了,数据库中的数据还是存在,显然不符合需求,此时虚引用就可以上线了

首先继承一下虚引用的类

static class UserPhantomReference extends PhantomReference<User> {
    // 保存user的databaseClient 因为取不到user对象
    public DatabaseClient databaseClient;

    public UserPhantomReference(User referent, ReferenceQueue<? super User> q) {
        super(referent, q);
        this.databaseClient = referent.databaseClient;
    }
}

这样主要是为了可以在对象删除时获取到databaseClient,才能实际删除数据,因为默认的PhantomReference是get不到数据的

运行代码

public static void main(String[] args) {
    // 新建一个对象,开辟一个内存空间
    User obj = new User();
    // 存储被回收的对象
    ReferenceQueue<User> QUEUE = new ReferenceQueue<>();
    // phantomReference使用虚引用指向这个内存空间
    UserPhantomReference phantomReference = new UserPhantomReference(obj, QUEUE);
    // 释放这个内存空间,此时只有phantomReference通过虚引用指向它
    obj = null;
    // 调用gc
    System.gc();
    // 被清除的队列中取出被回收的对象,一般新开一个线程来监控
    while (true) {
        Reference<? extends User> poll = QUEUE.poll();
        if (poll!=null) {
            UserPhantomReference userPhantomReference = (UserPhantomReference) poll;
            // 对象被回收,删除对应数据
            userPhantomReference.databaseClient.remove();
            System.out.println("--obj is recovery--");
        }
    }
}

此时输出:

--数据库创建用户数据--
--数据库删除用户数据--

说明对象在被GC回收时,对应的数据库数据也删掉了,满足了我们的需求

finalize

提到虚引用,就不得不提finalize方法,finalize方法也是在对象回收时被执行,通过实现finalize方法上面的需求很容易就搞定了,只需要User对象重写finalize方法即可

static class User {
    public DatabaseClient databaseClient;
    public User() {
        // 初始化客户端
        databaseClient = new DatabaseClient();
        // 创建时数据库创建数据
        this.databaseClient.create();
    }
    @Override
    protected void finalize() {
        // 销毁时删除数据
        this.databaseClient.remove();
    }
}

此时运行代码如下

public static void main(String[] args) {
    User obj = new User();
    // 释放这个内存空间
    obj = null;
    // 调用gc
    System.gc();
}

此时输出:

--数据库创建用户数据--
--数据库删除用户数据--

结果完全一致,而且对调度的代码几乎零侵入

那么finalize这么好用,写法这么简单,为啥还要有虚引用呐?

道理其实很简单,好用一般都不灵活,就像@Synchronized锁一样,用起来简单,但运行的规矩你是改不了的

同样finalize主要有以下几点不灵活:

最后

不管是虚引用也好,finalize也好,一般都是个兜底的方案,防止忘记手动关闭造成资源浪费,但最好还是要做手动关闭的,以防进行回收的线程意外停止

上一篇 下一篇

猜你喜欢

热点阅读