AbandonedConnectionCleanupThread$ConnectionFinalizerPhantomReference内存溢出

news/2025/2/12 2:46:19/

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

目录

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 spaceat 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];}

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

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) {//强转为 ConnectionFinalizerPhantomReferencefinalizeResource((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.ppmy.cn/news/103516.html

相关文章

dolphinscheduler3.1.7windows部署启动说明

简介 Apache DolphinScheduler是一个新一代分布式大数据工作流任务调度平台&#xff0c;致力于“解决大数据任务之间错综复杂的依赖关系&#xff0c;整个数据处理开箱即用”。它以 DAG(有向无环图) 的方式将任务连接起来&#xff0c;可实时监控任务的运行状态&#xff0c;同时…

二进制安装docker

二进制安装docker文档 建模部署 docker安装 下载docker 因rpm包安装依赖较多&#xff0c;选择二进制安装&#xff0c;下载地址如下 https://download.docker.com/linux/static/stable/x86_64/ 创建docker组 groupadd docker如果没有docker组&#xff0c;启动docker将会报…

java学习——java学习进度一String类1(学习记录——供回溯)

String 分割字符串 split( ) String s "1,2,3,4"; //未使用split分割前 System.out.println(s.length());//使用split分割后 String[] ssplit s.split(","); System.out.println(ssplit.length);split( , ) //两个参数都有的时候&#xff0c;第一个为用…

整合SSM(Spring + SpringMVC + Mybatis)

7、整合SSM 7.1、环境要求7.2、数据库环境7.3、基本环境搭建7.4、Mybatis层编写7.5、Spring层7.6、SpringMVC层7.7、小结及展望 7.1、环境要求 环境&#xff1a; IDEA MySQL 5.7.19 Tomcat 9 Maven 3.6 要求&#xff1a; 需要熟练掌握MySQL数据库&#xff0c;Spring&#…

[NOIP2004 普及组] FBI 树 递归解法

[NOIP2004 普及组] FBI 树 题目描述: 我们可以把由 0 和 1 组成的字符串分为三类&#xff1a;全 0 串称为 B 串&#xff0c;全 1 串称为 I 串&#xff0c;既含 0 又含 1 的串则称为 F 串。 FBI 树是一种二叉树&#xff0c;它的结点类型也包括 F 结点&#xff0c;B 结点和 I …

Jetson硬件 升级jetpack系统 版本流程

前言&#xff1a; 准备一个linux系统电脑&#xff0c;一个跳线帽&#xff0c;以及一条usb线 以及英伟达开发者账号&#xff0c;点击https://developer.nvidia.com/zh-cn/embedded/jetpack进行注册 注意&#xff1a;烧录的目标硬件是以前装过低版本的jetpack&#xff0c;以下步…

回溯递归的剪枝模版

题目传送门 主要看灵神的二分模版&#xff0c;如何使用递归实现在 O ( m k ) O(mk) O(mk)时间内&#xff0c;实现对于二分中每个条件的判断。 一般套路&#xff1a; dfs函数返回值为布尔类型 循环中使用一个dfs&#xff0c;如果其返回true&#xff0c;那么直接这个dfs返回tru…

Cache性能,多核,一致性

cache performance影响因素&#xff1a; 1.cache size 2.block size 3.组相连度&#xff0c;4.替换策略 目标 1.减少miss rate(可以用一个指针指向不常用的数据结构) 2.减少miss penalty 3.减少hit cost 多核系统下的cache设计 分布or集中 集中 优点 缺点 资源竞争,不平等…