【♨ Java基础】虚引用(PhantomReference)的使用场景

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

使用

public class TestMain {

    public static void main(String[] args) {
        // 新建一个对象
        User obj = new User();
        // 存储被回收的对象
        ReferenceQueue<User> QUEUE = new ReferenceQueue<>();
        // phantomReference使用虚引用指向这个内存空间
        PhantomReference<User> phantomReference = new PhantomReference<>(obj, QUEUE);
        System.out.println(phantomReference.get()); // 获取不到 打印为null
    }

    static class User {
       
    }
}

作用

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

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

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

// 新建一个对象,开辟一个内存空间
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--");
        break;
    }
}

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

其实,虚引用的存在意义就是监控对象是否存活。

场景

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

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

MySQLl驱动使用虚引用

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

写个例子

场景:我们的每创建一个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被回收了,数据库中的数据还是存在(没有调用remove方法),显然不符合需求,此时虚引用就可以上线了。

首先继承一下虚引用的类:

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方法执行的线程是不可控的
  • finalize方法的执行是串行执行的,使用虚引用我们可以并行多线程执行
  • finalize降低了gc效率,而虚引用不影响(实际上finalize执行时对象还没销毁)
不管是虚引用也好,finalize也好,一般都是个兜底的方案,防止忘记手动关闭造成资源浪费,但最好还是要做手动关闭的,以防进行回收的线程意外停止。
posted @ 2023-04-18 08:53  残城碎梦  阅读(1438)  评论(0编辑  收藏  举报