背景
在之前文章中写过 MySQL JDBC 驱动中的虚引用导致 JVM GC 耗时较长的问题,在驱动代码(mysql-connector-java 5.1.38版本)中 NonRegisteringDriver 类有个虚引用集合 connectionPhantomRefs 用于存储所有的数据库连接,NonRegisteringDriver.trackConnection 方法负责把新创建的连接放入集合,虚引用随着时间积累越来越多,导致 GC 时处理虚引用的耗时较长,影响了服务的吞吐量:
publicConnectionImpl(StringhostToConnectTo,intportToConnectTo,Propertiesinfo,StringdatabaseToConnectTo,Stringurl)throwsSQLException{ ... NonRegisteringDriver.trackConnection(this); ... }
publicclassNonRegisteringDriverimplementsDriver{ ... protectedstaticfinalConcurrentHashMapconnectionPhantomRefs=newConcurrentHashMap(); protectedstaticvoidtrackConnection(com.mysql.jdbc.ConnectionnewConn){ ConnectionPhantomReferencephantomRef=newConnectionPhantomReference((ConnectionImpl)newConn,refQueue); connectionPhantomRefs.put(phantomRef,phantomRef); } ... }
尝试减少数据库连接的生成速度,来降低虚引用的数量,但是效果并不理想。最终的解决方案是通过反射获取虚引用集合,利用定时任务来定期清理集合,避免 GC 处理虚引用耗时较长。
//每两小时清理connectionPhantomRefs,减少对mixedGC的影响 SCHEDULED_EXECUTOR.scheduleAtFixedRate(()->{ try{ FieldconnectionPhantomRefs=NonRegisteringDriver.class.getDeclaredField("connectionPhantomRefs"); connectionPhantomRefs.setAccessible(true); Mapmap=(Map)connectionPhantomRefs.get(NonRegisteringDriver.class); if(map.size()>50){ map.clear(); } }catch(Exceptione){ log.error("connectionPhantomRefsclearerror!",e); } },2,2,TimeUnit.HOURS);
利用定时任务清理虚引用效果立竿见影,每日几亿请求的服务 mixed GC 耗时只有 10 - 30 毫秒左右,系统也很稳定,线上运行将近一年没有任何问题。
优化——暴力破解到优雅配置
最近又有同事遇到相同的问题,使用的 mysql-connector-java 版本与我们使用的版本一致,查看最新版本(8.0.32)的代码发现对数据库连接的虚引用有新的处理方式,不像老版本(5.1.38)中每一个连接都会生成虚引用,而是可以通过参数来控制是否需要生成。类 AbandonedConnectionCleanupThread 的相关代码如下:
//静态变量通过System.getProperty获取配置 privatestaticbooleanabandonedConnectionCleanupDisabled=Boolean.getBoolean("com.mysql.cj.disableAbandonedConnectionCleanup"); publicstaticbooleangetBoolean(Stringname){ returnparseBoolean(System.getProperty(name)); } protectedstaticvoidtrackConnection(MysqlConnectionconn,NetworkResourcesio){ //判断配置的属性值来决定是否需要生成虚引用 if(!abandonedConnectionCleanupDisabled){ ··· ConnectionFinalizerPhantomReferencereference=newConnectionFinalizerPhantomReference(conn,io,referenceQueue); connectionFinalizerPhantomRefs.add(reference); ··· } }
mysql-connector-java 的维护者应该是注意到了虚引用对 GC 的影响,所以优化了代码,让用户可以自定义虚引用的生成。
有了这个配置,就可以在启动参数上设置属性:
java-jarapp.jar-Dcom.mysql.cj.disableAbandonedConnectionCleanup=true
或者在代码里设置属性:
System.setProperty(PropertyDefinitions.SYSP_disableAbandonedConnectionCleanup,"true");
当 com.mysql.cj.disableAbandonedConnectionCleanup=true 时,生成数据库连接时就不会生成虚引用,对 GC 就没有任何影响了。
建议还是使用第一种方式,通过启动参数配置更灵活一点。
什么是虚引用
有些读者看到这里知道 mysql-connector-java 生成的虚引用对 GC 有一些副作用,但是还不太了解虚引用到底是什么,有什么作用,这里我们在虚引用上做一点点拓展。
Java 虚引用(Phantom Reference)是Java中一种特殊的引用类型,它是最弱的一种引用。与其他引用不同,虚引用并不会影响对象的生命周期,也不会影响对象的垃圾回收。虚引用主要用于在对象被回收时收到系统通知,以便在回收时执行一些必要的清理工作。
上述虚引用的定义还是比较难理解,我们用代码来辅助理解:
先来生成一个虚引用:
//虚引用队列 ReferenceQueue
虚引用的构造方法需要两个入参,第一个就是关联的对象、第二个是虚引用队列 ReferenceQueue。虚引用需要和 ReferenceQueue 配合使用,当对象 Object o 被垃圾回收时,与 Object o 关联的虚引用就会被放入到 ReferenceQueue 中。通过从 ReferenceQueue 中是否存在虚引用来判断对象是否被回收。
我们再来理解上面对虚引用的定义,虚引用不会影响对象的生命周期,也不会影响对象的垃圾回收。如果上述代码里的phantomReference 是一个普通的对象,那么在执行 System.gc() 时 Object o 一定不会被回收掉,因为普通对象持有 Object o 的强引用,还不会被作为垃圾。这里的 phantomReference 是一个虚引用的话 Object o 就会被直接回收掉。然后会将关联的虚引用放到队列里,这就是虚引用关联对象被回收时会收到系统通知的机制。
一些实践能力很强的读者会复制上述代码去运行,发现垃圾回收之后队列里并没有虚引用。这是因为 Object o 还在栈里,属于是 GC Root 的一种,不会被垃圾回收。我们可以这样改写:
staticReferenceQueuequeue=newReferenceQueue<>(); publicstaticvoidmain(String[]args)throwsInterruptedException{ PhantomReference phantomReference=buildReference(); System.gc();Thread.sleep(100); System.out.println(queue.poll()); } publicstaticPhantomReference buildReference(){ Objecto=newObject(); returnnewPhantomReference<>(o,queue); }
不在 main 方法里实例化关联对象 Object o,而是利用一个 buildReference 方法来实例化,这样在执行垃圾回收的时候,Object o 已经出栈了,不再是 GC Root,会被当做垃圾来回收。这样就能从虚引用队列里取出关联的虚引用进行后续处理。
关联对象真的被回收了吗
执行完垃圾回收之后,我们确实能从虚引用队列里获取到虚引用了,我们可以思考一下,与该虚引用关联的对象真的已经被回收了吗?
使用一个小实验来探索答案:
publicstaticvoidmain(String[]args){ ReferenceQueuequeue=newReferenceQueue<>(); PhantomReference phantomReference=newPhantomReference<>( newbyte[1024*1024*2],queue); System.gc();Thread.sleep(100L); System.out.println(queue.poll()); byte[]bytes=newbyte[1024*1024*4]; }
代码里生成一个虚引用,关联对象是一个大小为 2M 的数组,执行垃圾回收之后尝试再实例化一个大小为 4M 的数组。如果我们从虚引用队列里获取到虚引用的时候关联对象已经被回收,那么就能正常申请到 4M 的数组。(设置堆内存大小为 5M -Xmx5m -Xms5m)
执行代码输出如下:
java.lang.ref.PhantomReference@533ddba Exceptioninthread"main"java.lang.OutOfMemoryError:Javaheapspace atcom.ppphuang.demo.phantomReference.PhantomReferenceDemo.main(PhantomReferenceDemo.java:15)
从输出可以看到,申请 4M 内存的时候内存溢出,那么问题的答案就很明显了,关联对象并没有被真正的回收,内存也没有被释放。
再做一点小小的改造,实例化新数组的之前将虚引用直接置为 null,这样关联对象就能被真正的回收掉,也能申请足够的内存:
publicstaticvoidmain(String[]args){ ReferenceQueuequeue=newReferenceQueue<>(); PhantomReference phantomReference=newPhantomReference<>( newbyte[1024*1024*2],queue); System.gc();Thread.sleep(100L); System.out.println(queue.poll()); //虚引用直接置为null phantomReference=null; byte[]bytes=newbyte[1024*1024*4]; }
如果我们使用了虚引用,但是没有及时清理虚引用的话可能会导致内存泄露。
虚引用的使用场景——mysql-connector-java 虚引用源码分析
读到这里相信你已经了解了虚引用的一些基本情况,那么它的使用场景在哪里呢?
最典型的场景就是最开始写到的 mysql-connector-java 里处理 MySQL 连接的兜底逻辑。用虚引用来包装 MySQL 连接,如果一个连接对象被回收的时候,会从虚引用队列里收到通知,如果有些连接没有被正确关闭的话,就会在回收之前进行连接关闭的操作。
从 mysql-connector-java 的 AbandonedConnectionCleanupThread 类代码中可以发现并没有使用原生的 PhantomReference 对象,而是使用的是包装过的 ConnectionFinalizerPhantomReference,增加了一个属性 NetworkResources,这是为了方便从虚引用队列中的虚引用上获取到需要处理的资源。包装类中还有一个 finalizeResources 方法,用来关闭网络连接:
privatestaticclassConnectionFinalizerPhantomReferenceextendsPhantomReference{ //放置需要GC后后置处理的网络资源 privateNetworkResourcesnetworkResources; ConnectionFinalizerPhantomReference(MysqlConnectionconn,NetworkResourcesnetworkResources,ReferenceQueue super MysqlConnection>refQueue){ super(conn,refQueue); this.networkResources=networkResources; } voidfinalizeResources(){ if(this.networkResources!=null){ try{ this.networkResources.forceClose(); }finally{ this.networkResources=null; } } } }
AbandonedConnectionCleanupThread 实现了 Runnable 接口,在 run 方法里循环读取虚引用队列 referenceQueue 里的虚引用,然后调用 finalizeResource 方法来进行后置的处理,避免连接泄露:
publicvoidrun(){ while(true){ try{ ... Reference extends MysqlConnection>reference=referenceQueue.remove(5000L); if(reference!=null){ //强转为ConnectionFinalizerPhantomReference finalizeResource((ConnectionFinalizerPhantomReference)reference); } ... } } } privatestaticvoidfinalizeResource(ConnectionFinalizerPhantomReferencereference){ try{ //兜底处理网络资源 reference.finalizeResources(); reference.clear(); }finally{ //移除虚引用避免可能造成的内存溢出 connectionFinalizerPhantomRefs.remove(reference); } }
如果你希望在某些对象被回收的时候做一些后置工作,可以参考 mysql-connector-java 中的一些实现逻辑。
总结
本文简述了一种优雅解决 MySQL 驱动中虚引用导致 GC 耗时较长问题的解决方法、也根据自己的理解讲述了虚引用的作用、结合 MySQL 驱动的源码描述了虚引用的使用场景,希望对你能有所帮助。
审核编辑:刘清
-
JAVA
+关注
关注
19文章
2954浏览量
104511 -
MySQL
+关注
关注
1文章
800浏览量
26413 -
JVM
+关注
关注
0文章
157浏览量
12202
原文标题:MySQL驱动中虚引用GC耗时优化与源码分析
文章出处:【微信号:OSC开源社区,微信公众号:OSC开源社区】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论