MySQL 驱动中虚引用 GC 耗时优化与源码分析( 二 )

虚引用的构造方法需要两个入参,第一个就是关联的对象、第二个是虚引用队列 ReferenceQueue 。虚引用需要和 ReferenceQueue 配合使用,当对象 Object o 被垃圾回收时,与 Object o 关联的虚引用就会被放入到 ReferenceQueue 中 。通过从 ReferenceQueue 中是否存在虚引用来判断对象是否被回收 。
我们再来理解上面对虚引用的定义,虚引用不会影响对象的生命周期,也不会影响对象的垃圾回收 。如果上述代码里的phantomReference 是一个普通的对象,那么在执行 System.gc() 时 Object o 一定不会被回收掉,因为普通对象持有 Object o 的强引用,还不会被作为垃圾 。这里的 phantomReference 是一个虚引用的话 Object o 就会被直接回收掉 。然后会将关联的虚引用放到队列里,这就是虚引用关联对象被回收时会收到系统通知的机制 。
一些实践能力很强的读者会复制上述代码去运行,发现垃圾回收之后队列里并没有虚引用 。这是因为 Object o 还在栈里,属于是 GC Root 的一种,不会被垃圾回收 。我们可以这样改写:
static ReferenceQueue<Object> queue = new ReferenceQueue<>();public static void mAIn(String[] args) throws InterruptedException {PhantomReference<Object> phantomReference = buildReference();System.gc();Thread.sleep(100);System.out.println(queue.poll());}public static PhantomReference<Object> buildReference() {Object o = new Object();return new PhantomReference<>(o, queue);}不在 main 方法里实例化关联对象 Object o,而是利用一个 buildReference 方法来实例化,这样在执行垃圾回收的时候,Object o 已经出栈了,不再是 GC Root,会被当做垃圾来回收 。这样就能从虚引用队列里取出关联的虚引用进行后续处理 。
关联对象真的被回收了吗执行完垃圾回收之后,我们确实能从虚引用队列里获取到虚引用了,我们可以思考一下,与该虚引用关联的对象真的已经被回收了吗?
使用一个小实验来探索答案:
public static void main(String[] args) {ReferenceQueue<byte[]> queue = new ReferenceQueue<>();PhantomReference<byte[]> phantomReference = new PhantomReference<>(new byte[1024 * 1024 * 2], queue);System.gc();Thread.sleep(100L);System.out.println(queue.poll());byte[] bytes = new byte[1024 * 1024 * 4];}代码里生成一个虚引用,关联对象是一个大小为 2M 的数组,执行垃圾回收之后尝试再实例化一个大小为 4M 的数组 。如果我们从虚引用队列里获取到虚引用的时候关联对象已经被回收,那么就能正常申请到 4M 的数组 。(设置堆内存大小为 5M -Xmx5m -Xms5m)
执行代码输出如下:
java.lang.ref.PhantomReference@533ddbaException in thread "main" java.lang.OutOfMemoryError: Java heap space at com.ppphuang.demo.phantomReference.PhantomReferenceDemo.main(PhantomReferenceDemo.java:15)从输出可以看到,申请 4M 内存的时候内存溢出,那么问题的答案就很明显了,关联对象并没有被真正的回收,内存也没有被释放 。
再做一点小小的改造,实例化新数组的之前将虚引用直接置为 null,这样关联对象就能被真正的回收掉,也能申请足够的内存:
public static void main(String[] args) {ReferenceQueue<byte[]> queue = new ReferenceQueue<>();PhantomReference<byte[]> phantomReference = new PhantomReference<>(new byte[1024 * 1024 * 2], queue);System.gc();Thread.sleep(100L);System.out.println(queue.poll());//虚引用直接置为 nullphantomReference = null;byte[] bytes = new byte[1024 * 1024 * 4];}如果我们使用了虚引用,但是没有及时清理虚引用的话可能会导致内存泄露 。
虚引用的使用场景——mysql-connector-java 虚引用源码分析读到这里相信你已经了解了虚引用的一些基本情况,那么它的使用场景在哪里呢?
最典型的场景就是最开始写到的 mysql-connector-java 里处理 MySQL 连接的兜底逻辑 。用虚引用来包装 MySQL 连接,如果一个连接对象被回收的时候,会从虚引用队列里收到通知,如果有些连接没有被正确关闭的话,就会在回收之前进行连接关闭的操作 。
从 mysql-connector-java 的 AbandonedConnectionCleanupThread 类代码中可以发现并没有使用原生的 PhantomReference 对象,而是使用的是包装过的 ConnectionFinalizerPhantomReference,增加了一个属性 NetworkResources,这是为了方便从虚引用队列中的虚引用上获取到需要处理的资源 。包装类中还有一个 finalizeResources 方法,用来关闭网络连接:
private static class ConnectionFinalizerPhantomReference extends PhantomReference<MysqlConnection> {//放置需要GC后后置处理的网络资源private NetworkResources networkResources;ConnectionFinalizerPhantomReference(MysqlConnection conn, NetworkResources networkResources, ReferenceQueue<? super MysqlConnection> refQueue) {super(conn, refQueue);this.networkResources = networkResources;}void finalizeResources() {if (this.networkResources != null) {try {this.networkResources.forceClose();} finally {this.networkResources = null;}}}}


推荐阅读