0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

一种优雅解决MySQL驱动中虚引用导致GC耗时较长问题的方法

OSC开源社区 来源:DailyHappy 2023-12-20 09:52 次阅读

背景

在之前文章中写过 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中一种特殊的引用类型,它是最弱的一种引用。与其他引用不同,虚引用并不会影响对象的生命周期,也不会影响对象的垃圾回收。虚引用主要用于在对象被回收时收到系统通知,以便在回收时执行一些必要的清理工作。

上述虚引用的定义还是比较难理解,我们用代码来辅助理解:

先来生成一个虚引用:

//虚引用队列
ReferenceQueuequeue=newReferenceQueue<>();
//关联对象
Objecto=newObject();
//调用构造方法生成一个虚引用第一个参数就是关联对象第二个参数是关联队列
PhantomReferencephantomReference=newPhantomReference<>(o,queue);
//执行垃圾回收
System.gc();
//延时确保回收完毕
Thread.sleep(100L);
//当Objecto被回收时可以从虚引用队列里获取到与之关联的虚引用这里就是phantomReference这个对象
Referencepoll=queue.poll();

虚引用的构造方法需要两个入参,第一个就是关联的对象、第二个是虚引用队列 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{
PhantomReferencephantomReference=buildReference();
System.gc();Thread.sleep(100);
System.out.println(queue.poll());
}

publicstaticPhantomReferencebuildReference(){
Objecto=newObject();
returnnewPhantomReference<>(o,queue);
}

不在 main 方法里实例化关联对象 Object o,而是利用一个 buildReference 方法来实例化,这样在执行垃圾回收的时候,Object o 已经出栈了,不再是 GC Root,会被当做垃圾来回收。这样就能从虚引用队列里取出关联的虚引用进行后续处理。

关联对象真的被回收了吗

执行完垃圾回收之后,我们确实能从虚引用队列里获取到虚引用了,我们可以思考一下,与该虚引用关联的对象真的已经被回收了吗?

使用一个小实验来探索答案:

publicstaticvoidmain(String[]args){
ReferenceQueuequeue=newReferenceQueue<>();
PhantomReferencephantomReference=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<>();
PhantomReferencephantomReference=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,ReferenceQueuerefQueue){
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{
...
Referencereference=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

    文章

    2904

    浏览量

    102994
  • MySQL
    +关注

    关注

    1

    文章

    775

    浏览量

    26004
  • JVM
    JVM
    +关注

    关注

    0

    文章

    152

    浏览量

    12129

原文标题:MySQL驱动中虚引用GC耗时优化与源码分析

文章出处:【微信号:OSC开源社区,微信公众号:OSC开源社区】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    如何卸载kb967723补丁 不能连接MYSQL

    掉线,经排查,并没有哪个网站受攻击,网上搜索了下,很多人都碰到这问题,全是Windows系统上安装MySQL,原因是前段时间微软发布的补丁KB967723而导致的。现在有两解决
    发表于 01-17 11:43

    步进电机5驱动方法的利弊分析

    这5驱动方法的利弊,以选择合适的驱动方法来做电机驱动。1. 恒电压
    发表于 01-27 14:45

    漫谈电路、信号处理的“部”

    没有问题,数学是对世界的一种描述,是抽象出来的,又如直线、空间等等概念也是抽象出来的。但现实世界里面的物理量,电流电压都是实际存在的,哪来的部呢(别扯到量子物理,不在电子工程讨论范围)? 更后来修
    发表于 10-25 09:31

    高效简洁的用labview读取excel工作簿信息

    读取excel工作簿的传统方法是用自动化引用进行数据读取操作。但是自动化引用链路较长,且由于Microsoft offer版本问题导致部分用
    发表于 04-11 18:24

    C++服务编译耗时优化的原理和服务分析

    编译的耗时。通过这个方式能够找到各个文件编译耗时的共性,下图是编译展开后文件大小截图。3.2 头文件依赖分析头文件依赖分析是从引用头文件数量的角度来看代码是否合理的一种分析方式,我们实
    发表于 12-23 17:32

    一种伺服电机的控制方法

    的采用的是BLDC控制方法,这 是一种基于方波的驱动控制方式。这种控制方式,启动转速高,电机的转矩与转速有定的 关系,要获得比较大的扭矩,电机需要的转速就要比较高,在低速运行时电机输
    发表于 09-03 08:53

    怎样去更改电源管理相关设置导致CPU处于满载的状态

    是否有遇到如下的情况,并且无论关闭后台多少任务,甚至重启都无法解决。本人认为这是超频软件更改了电源管理相关设置导致CPU处于"满载"的状态。下面是些解决
    发表于 12-27 07:36

    一种优雅的方式去实现个Verilog版的状态机

    描述:基于此,我们便可以方便快捷的去描述状态机,以一种优雅的方式去实现状态机描述,而对于他人阅读来讲也是相当OK的。等等,还有更好玩儿的。在SpinalHDL里,定义了四可以声明状态的类型
    发表于 07-13 14:56

    解密方舟的高性能内存回收技术——HPP GC

    和高性能,HPP GC采取了两项优化措施:措施:在新增引用关系时增加标记屏障(Marking Barrier),以确保标记结果的正确性。并发标记过程,JS线程有可能会更改对象之间的
    发表于 07-20 10:44

    一种更通用的方法来监测处理器的电压噪声

    可用的片上电容量。这导致了系统共振频率的急剧变化—个可以在电磁频谱捕捉到的强信号。该技术是非侵入性的,因为它不会与被监视的CPU进行物理交互。因此,它为计算机体系结构开辟了一种全新
    发表于 11-01 14:48

    flashDB / Easyflash触发GC导致系统卡死的解决方法分享

    来专门执行gc与Collect操作?就像ulog的异步线程的方式那样。写入数据库并不要求实时性,只需要确保数据的准确就好了。异步方式需要个缓冲区,缓冲区满了才能传入数据,可能导致缓冲区未满时掉电
    发表于 11-21 15:04

    什么是PCBA焊?解决PCBA焊的方法介绍

    层。它们没有完全接触在起。肉眼般无法看出其状态。 但是其电气特性并没有导通或导通不良。影响电路特性。  PCBA焊是常见的一种线路故障,有两
    发表于 04-06 16:25

    一次JVM GC长暂停的排查过程

    在高并发下,Java 程序的 GC 问题属于很典型的一类问题,带来的影响往往会被进一步放大。不管是「GC 频率过快」还是「GC 耗时太长」,由于 G
    的头像 发表于 01-17 10:08 378次阅读

    MySQL并发Replace into导致死锁场景简析

    在之前的文章 #issue 68021 MySQL unique check 问题中, 我们已经介绍了在 MySQL 里面, 由于唯一键的检查(unique check), 导致 MySQL
    的头像 发表于 06-13 10:56 630次阅读
    <b class='flag-5'>MySQL</b>并发Replace into<b class='flag-5'>导致</b>死锁场景简析

    导致MySQL索引失效的情况以及相应的解决方法

    导致MySQL索引失效的情况以及相应的解决方法  MySQL索引的目的是提高查询效率,但有些情况下索引可能会失效,导致查询变慢或效果不如预期
    的头像 发表于 12-28 10:01 308次阅读