java虚引用
前言
之前一篇文章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方法执行的线程是不可控的
- finalize方法的执行是串行执行的,使用虚引用我们可以并行多线程执行
- finalize降低了gc效率,而虚引用不影响(实际上finalize执行时对象还没销毁)
最后
不管是虚引用也好,finalize也好,一般都是个兜底的方案,防止忘记手动关闭造成资源浪费,但最好还是要做手动关闭的,以防进行回收的线程意外停止