AbandonedConnectionCleanupThread$ConnectionFinalizerPhantomReference内存溢出

news2024/11/26 2:35:29

网上查了查资料,根据自己情况在这里整理了一下,供大家学习和参考。

目录

1、现象

 2、mysql-connector-java 源码分析

3、解决方法

3.1、配置disableAbandonedConnectionCleanup

3.2、暴力解决方式-----定时GC

4、什么是虚引用

5、关联对象真的被回收了吗

6、虚引用的使用场景——mysql-connector-java 虚引用源码分析

7、参考:


1、现象

        最近发现,服务器内存一直在增加,dump内存后发现com.mysql.cj.jdbc.AbandonedConnectionCleanupThread$ConnectionFinalizerPhantomReference占用很多内存,

 2、mysql-connector-java 源码分析

        使用的 mysql-connector-java 版本(8.0.30)的代码发现对数据库连接的虚引用有新的处理方式,不像老版本(5.1.38)中每一个连接都会生成虚引用,而是可以通过参数来控制是否需要生成。类 AbandonedConnectionCleanupThread  的相关代码如下:

//静态变量通过 System.getProperty 获取配置
private static boolean abandonedConnectionCleanupDisabled = Boolean.getBoolean("com.mysql.cj.disableAbandonedConnectionCleanup");

public static boolean getBoolean(String name) {
      return parseBoolean(System.getProperty(name));
}

protected static void trackConnection(MysqlConnection conn, NetworkResources io) {
    //判断配置的属性值来决定是否需要生成虚引用
      if (!abandonedConnectionCleanupDisabled) {
         ···
          ConnectionFinalizerPhantomReference reference = new ConnectionFinalizerPhantomReference(conn, io, referenceQueue);
          connectionFinalizerPhantomRefs.add(reference);
         ··· 
      }
  }

        mysql-connector-java 的维护者应该是注意到了虚引用对 GC 的影响,所以优化了代码,让用户可以自定义虚引用的生成。

3、解决方法

3.1、配置disableAbandonedConnectionCleanup

When using Connector/J, the AbandonedConnectionCleanupThread thread can now be disabled completely by setting the new system property com.mysql.disableAbandonedConnectionCleanup to true when configuring the JVM. The feature is for well-behaving applications that always close all connections they create. 

有了这个配置,就可以在启动参数上设置属性:

java -jar app.jar -Dcom.mysql.cj.disableAbandonedConnectionCleanup=true

或者在代码里设置属性:

System.setProperty(PropertyDefinitions.SYSP_disableAbandonedConnectionCleanup,"true");

        当 com.mysql.cj.disableAbandonedConnectionCleanup=true 时,生成数据库连接时就不会生成虚引用,对 GC 就没有任何影响了。

        建议还是使用第一种方式,通过启动参数配置更灵活一点。

3.2、暴力解决方式-----定时GC

        在之前文章中写过 MySQL JDBC 驱动中的虚引用导致 JVM GC 耗时较长的问题(可以看这里),在驱动代码(mysql-connector-java 5.1.38版本)中 NonRegisteringDriver 类有个虚引用集合 connectionPhantomRefs 用于存储所有的数据库连接,NonRegisteringDriver.trackConnection 方法负责把新创建的连接放入集合,虚引用随着时间积累越来越多,导致 GC 时处理虚引用的耗时较长,影响了服务的吞吐量:

public ConnectionImpl(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url) throws SQLException {
 ...
 NonRegisteringDriver.trackConnection(this);
  ...
}
public class NonRegisteringDriver implements Driver {
  ...
  protected static final ConcurrentHashMap<ConnectionPhantomReference, ConnectionPhantomReference> connectionPhantomRefs = new ConcurrentHashMap();
   
  protected static void trackConnection(com.mysql.jdbc.Connection newConn) {
        ConnectionPhantomReference phantomRef = new ConnectionPhantomReference((ConnectionImpl)newConn, refQueue);
        connectionPhantomRefs.put(phantomRef, phantomRef);
    }
  ...
}

        尝试减少数据库连接的生成速度,来降低虚引用的数量,但是效果并不理想。最终的解决方案是通过反射获取虚引用集合,利用定时任务来定期清理集合,避免 GC 处理虚引用耗时较长。

// 每两小时清理 connectionPhantomRefs,减少对 mixed GC 的影响
SCHEDULED_EXECUTOR.scheduleAtFixedRate(() -> {
  try {
    Field connectionPhantomRefs = NonRegisteringDriver.class.getDeclaredField("connectionPhantomRefs");
    connectionPhantomRefs.setAccessible(true);
    Map map = (Map) connectionPhantomRefs.get(NonRegisteringDriver.class);
    if (map.size() > 50) {
      map.clear();
    }
  } catch (Exception e) {
    log.error("connectionPhantomRefs clear error!", e);
  }
}, 2, 2, TimeUnit.HOURS);

        利用定时任务清理虚引用效果立竿见影,每日几亿请求的服务 mixed GC 耗时只有 10 - 30 毫秒左右,系统也很稳定,线上运行将近一年没有任何问题。

4、什么是虚引用

        有些读者看到这里知道 mysql-connector-java 生成的虚引用对 GC 有一些副作用,但是还不太了解虚引用到底是什么,有什么作用,这里我们在虚引用上做一点点拓展。

        Java 虚引用(Phantom Reference)是Java中一种特殊的引用类型,它是最弱的一种引用。与其他引用不同,虚引用并不会影响对象的生命周期,也不会影响对象的垃圾回收。虚引用主要用于在对象被回收时收到系统通知,以便在回收时执行一些必要的清理工作。

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

        先来生成一个虚引用:

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

        虚引用的构造方法需要两个入参,第一个就是关联的对象、第二个是虚引用队列 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,会被当做垃圾来回收。这样就能从虚引用队列里取出关联的虚引用进行后续处理。

5、关联对象真的被回收了吗

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

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

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@533ddba
Exception 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());
    //虚引用直接置为 null
    phantomReference = null;
      byte[] bytes = new byte[1024 * 1024 * 4];
  }

如果我们使用了虚引用,但是没有及时清理虚引用的话可能会导致内存泄露

6、虚引用的使用场景——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;
              }
          }
      }
  }

        AbandonedConnectionCleanupThread 实现了 Runnable 接口,在 run 方法里循环读取虚引用队列 referenceQueue 里的虚引用,然后调用 finalizeResource 方法来进行后置的处理,避免连接泄露:

public void run() {
    while(true) {
        try {
           ...
            Reference<? extends MysqlConnection> reference = referenceQueue.remove(5000L);
            if (reference != null) {
               //强转为 ConnectionFinalizerPhantomReference
                finalizeResource((ConnectionFinalizerPhantomReference)reference);
            }
           ...
        }
    }
}

private static void finalizeResource(ConnectionFinalizerPhantomReference reference) {
    try {
       //兜底处理网络资源
        reference.finalizeResources();
        reference.clear();
    } finally {
       //移除虚引用 避免可能造成的内存溢出
        connectionFinalizerPhantomRefs.remove(reference);
    }
}

        如果你希望在某些对象被回收的时候做一些后置工作,可以参考 mysql-connector-java 中的一些实现逻辑。

7、参考:

New minimumIdle connections every maxLifetime causing MySQL'AbandonedConnectionCleanupThread Memory Leak · Issue #1473 · brettwooldridge/HikariCP · GitHub

MySQL 驱动中虚引用 GC 耗时优化与源码分析 | HeapDump性能社区

MySQL Connector内存增长问题排查 - 知乎

数据库虚引用堆积问题 | 技术人生

 MySQL连接池引起的FullGC问题分析 - 掘金

8.内存溢出_鹏哥哥啊Aaaa 的博客-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/584579.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据可视化:趋势类可视化图表大全

图表是处理数据的重要组成部分&#xff0c;因为它们是一种将大量数据压缩为易于理解的格式的方法。数据可视化可以让受众快速Get到重点。 数据可视化的图表类型极其丰富多样&#xff0c;而且每种都有不同的用例&#xff0c;通常&#xff0c;创建数据可视化最困难的部分是确定哪…

【Linux】常用命令的汇总学习

文章目录 1.目录切换命令2.目录操作命令3.把ls -l中包含字母file&#xff08;不区分大小写&#xff09;的内容输出4.统计txt中的某个字符串5.grep命令的使用6.linux查找当前目录下所有txt文件7.linux中的find命令8.查看系统所有的进程信息9.如何确定文件的类型10.tar解压缩11.U…

员工防范网络钓鱼攻击的10个实用技巧

你知道网络钓鱼攻击的危害吗&#xff1f;以下是网络钓鱼的定义及其引起关注的原因&#xff1a; Verizon 最近的一份报告显示&#xff0c;82&#xff05;的网络漏洞均由人为因素造成&#xff0c;比如窃取凭证、网络钓鱼攻击、社会工程学、冒名顶替、滥用或错误等。网络钓鱼攻击…

惊!掌握千问通义的关键,从这些必知内容开始!

今年快过半了&#xff0c;要说顶流话题还得是ChatGPT&#xff0c;相关话题的热度居高不下&#xff0c;而其从GPT-3.5到GPT-4的升级&#xff0c;也让我们深刻了解了什么叫一代版本一代神&#xff0c;从GPT-3.5到GPT-4&#xff0c;真的就是一个跨阶级式的升级。 技术内涵 ChatGPT…

某SRC的渗透测试实战

前言 因为不甘心被称作会只点鼠标的猴子&#xff0c;所以开始了一次某SRC漏洞挖掘&#xff0c;为期一个多星期。文章有点长&#xff0c;但请耐心看完&#xff0c;记录了完整的SRC漏洞挖掘实战 渗透过程 因为选择的幸运儿没有对测试范围进行规划&#xff0c;所以此次范围就是…

OPC UA客户端访问 OPC DA服务器

目标 用OPC UA客户端&#xff08;如UaExpert&#xff09;读取OPC DA服务器上的点。 原理 OPC DA是基于COM/DCOM的&#xff0c;传统OPC DA客户端访问非本机OPC DA服务时需要配置DCOM。OPC UA客户端无法直接访问 OPC DA服务&#xff0c;需要将OPC DA服务映射为OPC UA服务&#x…

【Excel技巧】3个限制权限,保护表格不被人随意改动

Excel表格是很多人工作中经常用到的办公软件&#xff0c;有时候做好表格发给对方后&#xff0c;总是担心会被不小心做了改动。 如果有这种顾虑&#xff0c;就一定要用上Excel表格的3个“限制权限”&#xff0c;可以根据不同的情况&#xff0c;设置不同保护。下面就来看看可以设…

LAMP配置安装

目录 一&#xff1a;LAMP 1、(平台)Linux 2、(前台)Apache 3、(后台)MySQL 4、(中间连接)PHP/Perl/Python 5. Lamp工作原理 二&#xff1a;编译安装Apache httpd服务 1.关闭防火墙&#xff0c;将安装Apache所需软件包传到/opt目录下 2.安装环境依赖包 3.配置软件模块…

打造繁荣社区:Solaris 与 Web3 合作的力量

在去中心化金融&#xff08;DeFi&#xff09;的动态格局中&#xff0c;Solaris 作为一股开创性力量涌现&#xff0c;为衍生品提供了强大的 Web3 基础设施。Solaris 成功的关键在于其充满活力且迅速增长的社区&#xff0c;该社区在塑造平台影响力和促进创新方面发挥着关键作用。…

C++ 新特性

1.auto、decltype 用于自动推断类型 2.自动追踪返回值类型 3. 列表初始化和列表方式类型收窄 //列表初始化 vector<int>res{1,2,3,4,5}; //防止类型收窄 int a 1024; char b a;//可以执行 char b{a};//报错 4.基于范围的for循环 vector<int>res{1,2,3,4}; fo…

“外行转网工,我只用了三个月”

大家好&#xff0c;我是老杨。 在这行发展了这么多年&#xff0c;经常会有人来问我&#xff0c;网工该怎么提升自己&#xff0c;又或是怎么入行。 其实这事儿不难想&#xff0c;技术工种最需要做的是什么&#xff0c;自然是提升技术。 而技术提升&#xff0c;途径也只有学习…

k维空间中的超平面的维度是多少?

超平面中的任何点 x x x可以用一个法向量 w w w和超平面上的一个点 x 0 x_0 x0​表示. x x x可以用 w w w和 x 0 x_0 x0​表示&#xff1a; ( x − x 0 ) ⋅ w 0 w T x − w T x 0 0 w T x b 0 \begin{split} (x-x_0) \cdot w &0 \\ w^Tx-w^Tx_0 &0 \\ w^Txb &a…

高压断路器工作运行要求是什么

1、断路器应在规定的技术参数范围内运行。 2、断路器必须配备可靠的操作电源和合闸电源。 3、送电应先合刀闸后合断路器&#xff1b;停电先断开断路器&#xff0c;检查确已断开后再拉刀闸。 4、110kV高压设备的巡视检查可由一人进行&#xff0c;但距带电体的距离应不小于1.5…

三分钟了解Spring Cloud Gateway路由转发之自动路由

文章目录 一、前言二、路由配置1. 静态路由2. 动态路由3. 自动路由 三、Spring Cloud Gateway 是如何实现动态路由工作原理源码解析路由转发原理路由转发源码解析 四 、问题核心DiscoveryClientRouteDefinitionLocator源码解析 五、总结 大家好&#xff0c;我是冰点&#xff0c…

领导者指南:用四步空杯学习法避免你的成就成为累赘

好的领导者需要不断学习。而优秀的领导者更知道什么时候忘记过去&#xff0c;才能在未来取得成功。经营任何成功都存在学习曲线。但是&#xff0c;一旦你开始依赖过去的成就&#xff0c;或者陷入过时的思维和实践&#xff0c;不再起作用&#xff0c;你必须退后一步——空杯学习…

安捷伦E4440A(Agilent) e4440a 3HZ-26.5G频谱分析仪

Agilent E4440A、Keysight E4440A、HP E4440A频谱分析仪&#xff0c;3 Hz - 26.5 GHz&#xff08;PSA 系列&#xff09; ​Agilent / Keysight PSA 系列 E4440A 高性能频谱分析仪提供强大的一键式测量、多功能功能集和前沿技术&#xff0c;可满足您的项目和需求。选项可供您选…

Python实战基础14-递归函数

1、什么是递归函数 如果一个函数在内部不调用其它的函数&#xff0c;而是自己本身的话&#xff0c;这个函数就递归函数。 遵循&#xff1a; 必须要有出口每次递归向出口靠近 # 1-10 打印数字 def test(i):if i 10:print(10)else:print(i)i 1test(i) test(1)# 1-10 的累加…

海外网红营销ROI评估:出海品牌如何科学衡量投资回报?

随着全球化的不断推进&#xff0c;出海品牌越来越多地将目光投向海外市场&#xff0c;并利用海外网红的影响力进行营销。然而&#xff0c;对于出海品牌而言&#xff0c;如何科学衡量海外网红营销的投资回报率&#xff08;ROI&#xff09;却是一个关键的挑战。本文Nox聚星将和大…

Phaser笔记-精灵(sprite)的使用

如下&#xff1a; 使用逻辑如下&#xff1a; ①设置好physics&#xff1b; ②将资源添加到spritesheet&#xff1b; ③在physics中添加sprite&#xff1b; ④创建anims&#xff1b; ⑤播放anims&#xff1b; 完整代码如下&#xff1a; import phaser;class PlayGame exte…

进程间通信(命名管道)

目录&#xff1a; 1.命名管道 2.创建命名管道 --------------------------------------------------------------------------------------------------------------------------------- 1.命名管道 1.管道的一个应用限制就是只能在具有共同祖先&#xff08;具有亲缘关系&…