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

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

3天内不再提示

虚引用GC耗时分析优化(由 1.2 降低至 0.1 秒)

京东云 ? 来源:jf_75140285 ? 作者:jf_75140285 ? 2025-08-14 11:18 ? 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

背景

线上应用频繁出现超时告警(超时时间 1 s): getUiToken 接口异常状态码“-1”出现4037次(失败描述:业务请求异常),超过阈值50,协议:http,为服务端接口。当前失败率为0%,当前平均响应时间为150ms,TP50为2ms,TP90为896ms,TP99为1024ms,TP999为1152ms,MAX为1280ms。

环境信息

?服务器配置为,Linux 4c8g 标配机器

?JVM 参数配置:

-server -Djava.library.path=/usr/local/lib -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/export/log -Djava.awt.headless=true -Dsun.net.client.defaultConnectTimeout=60000 -Dsun.net.client.defaultReadTimeout=60000 -Djmagick.systemclassloader=no -Dnetworkaddress.cache.ttl=300 -Dsun.net.inetaddr.ttl=300 -Xms5G -Xmx5G -XX:+UseG1GC -XX:G1HeapRegionSize=4m -Xloggc:/export/log/$APP_NAME/gc_detail.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10m -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution -XX:+PrintHeapAtGC

?接口流量情况:

wKgZO2idVWeAB135AADKM5AFg5Q834.png

问题排查

分析代码实现,getUiToken 只是简单的内存数据获取并返回,没有其他复杂操作,不存在有耗时操作

wKgZPGidVWiAJbZaAAGtbsIAkGM982.png

接着借助 SGM 监控平台分析下接口耗时情况,发现方法执行的耗时都处在等待中,为什么会有这多长的等待时间呢?

wKgZO2idVWiAYfMUAALKqNL9w28122.png

业务代码很简单,只是内存级别的获取,序列化和反序列化也不可能耗时这么久, 难道是 GC 的原因 ? 因为 JVM 之前就配置了 -Xloggc 日志输出,所以先分析下 GC 日志情况,看看 GC 是否存在异常情况(工具 gcViewer)

wKgZPGidVWqAAmwUAASJkrEETGo651.png

果然,光看吞吐量 99.3% 和 full gc 次数 0,感觉性能好像还不错的,但 4227 次 young gc (平均 20 多秒一次) 和 max 1.25 秒的耗时,就有点离谱,分析 GC 日志详情

wKgZPGidVWuAaBaDAALVfuTGOCY238.png

随机搜查了一个 GC 耗时在 1s 以上的日志,发现 1.02 s 的 gc 有 977.3 ms 是在处理 Ref Proc,为什么这个耗时会这么长 ?

ref-proc 是对各种软弱虚引用等的处理,处理 soft、weak、phantom、final、JNI 等等引用的时间

具体是那种引用类型的耗时长,可以通过增加 JVM 参数 -XX:+PrintReferenceGC 打印引用GC类型 ,重新上线一段时间后观察GC日志

wKgZO2idVWyADigGAALONKOTEww021.png

发现耗时基本都在 PhantomReference 类型的对象上,为什么会有这么多这个对象?

PhantomReference 是什么? 1、虚引用也称为“幽灵引用”,它是最弱的一种引用关系。 2、如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 3、为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。 4、当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收后,将这个虚引用加入引用队列,在其关联的虚引用出队前,不会彻底销毁该对象。所以可以通过检查引用队列中是否有相应的虚引用来判断对象是否已经被回收了。

为了搞清楚 PhantomReference 具体都是哪些对象,我们需要 dump 堆文件查看对象的分布(内存分析工具 MAT),dump 时最好将流量摘除后操作,以免影响线上。查看直方图中的对象发现有 4340 个虚引用对象,和GC日志中的数量基本对的上。

wKgZPGidVW2AWhbJAALB-9mB6-Q934.png

那为什么会有这么多 ConnectionPhantomReference 对象呢 ? 这个东西有什么用呢 ? 源码如下 (mysql-connector-java 版本 5.1.44)

// 相关的核心逻辑
public class NonRegisteringDriver implements java.sql.Driver {
    protected static final ConcurrentHashMap connectionPhantomRefs = new ConcurrentHashMap();
    protected static final ReferenceQueue refQueue = new ReferenceQueue(); 

    protected static void trackConnection(Connection newConn) {
        ConnectionPhantomReference phantomRef = new ConnectionPhantomReference((ConnectionImpl) newConn, refQueue);
        connectionPhantomRefs.put(phantomRef, phantomRef);
    }
}

public class ConnectionImpl extends ConnectionPropertiesImpl implements MySQLConnection {
  public ConnectionImpl(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url) throws SQLException {
    ...
    NonRegisteringDriver.trackConnection(this);
  }
}

通过代码发现,每次新建数据库连接 Connection 时都会向 connectionPhantomRefs 存放一个对象,那么已经使用 druid (版本:1.0.15)线程池为什么还会生成这么多新链接 ? 接着查看项目中的数据库配置,发现数据源使用的是默认配置

# 线程池使用的默认配置
spring:
  datasource:
    url: jdbc:mysql://xxxxxxxxx?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&serverTimezone=GMT%2B8
    username: xxxx
    password: xxxx
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

wKgZO2idVW6AIus_AAHqfMbMbjk443.png

默认配置中,线程池中的连接存活时间为 30 分钟,最小闲置线程数为 0,因此虽使用了线程池也还是会不断的创建新的链接,并增加到 connectionPhantomRefs 集合中,累计一定量之后就会影响 ref-proc 耗时。

如何解决 ?

既然因为它累计的量大导致 gc 时长变高,那么我们分析下 connectionPhantomRefs 的作用到底是什么 ?

跟进 connectionPhantomRefs 引用处理对象,发现 connectionPhantomRefs 中的元素都是被 AbandonedConnectionCleanupThread 处理的

/**
 * This class implements a thread that is responsible for closing abandoned MySQL connections,
 * i.e., connections that are not explicitly closed. 
 * There is only one instance of this class and there is a single thread to do this task. 
 * This thread's executor is statically referenced in this same class.
 *
 * 该类实现了一个负责关闭被遗弃的MySQL连接的线程,即未显式关闭的连接。该类的实例只有一个,并且由单个线程执行此任务。该线程的执行器在此类中以静态方式引用。
 */
public class AbandonedConnectionCleanupThread implements Runnable {
    private static final ExecutorService cleanupThreadExcecutorService;
    static Thread threadRef = null;

    static {
        cleanupThreadExcecutorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r, "Abandoned connection cleanup thread");
                t.setDaemon(true);
                t.setContextClassLoader(AbandonedConnectionCleanupThread.class.getClassLoader());
                return threadRef = t;
            }
        });
        cleanupThreadExcecutorService.execute(new AbandonedConnectionCleanupThread());
    }

    public void run() {
        for (;;) {
            try {
                checkContextClassLoaders();
                Reference ref = NonRegisteringDriver.refQueue.remove(5000);
                if (ref != null) {
                    try {
                        ((ConnectionPhantomReference) ref).cleanup();
                    } finally {
                        NonRegisteringDriver.connectionPhantomRefs.remove(ref);
                    }
                }
            } catch (InterruptedException e) {
                threadRef = null;
                return;
            } catch (Exception ex) {
                // Nowhere to really log this.
            }
        }
    }
}

根据 com.mysql.jdbc.AbandonedConnectionCleanupThread 类注释信息,发现该类是 mysql 连接的兜底处理逻辑,负责关闭被遗弃的MySQL连接,主要有两种情况

1. 未显式关闭的连接(代码遗漏)

2. 异常未处理的连接(程序崩溃/未捕获异常/网络或事务超时导致的连接失效)

解决方案

1. 优化 druid 连接池连接数配置 (按实际流量情况评估),增加连接存活时长到 59 分钟(MySql wait_timeout 默认为 3600 秒),最大限度减少新连接生成

spring:
  datasource:
    url: jdbc:mysql://xxxxxxxxx?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&serverTimezone=GMT%2B8
    username: xxxx
    password: xxxx
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      # 增加线程数
      minIdle: 4
      maxActive: 10
      initialSize: 4
      # 启动检测线程回收
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      validationQuery: select 1
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 3540000

2. 开启 ref-proc 并行处理:-XX:+ParallelRefProcEnabled , G1 官网建议

Reference Object Processing Takes Too Long Information about the time taken for processing of Reference Objects is shown in the Ref Proc and Ref Enq phases. During the Ref Proc phase, G1 updates the referents of Reference Objects according to the requirements of their particular type. In Ref Enq, G1 enqueues Reference Objects into their respective reference queue if their referents were found dead. If these phases take too long, then consider enabling parallelization of these phases by using the option -XX:+ParallelRefProcEnabled.

3. 由于 connectionPhantomRefs 只是作为异常情况出现的兜底处理,我们项目中使用的线程池都会自动处理资源释放的情况,不存在手动操作的情况,是不是可以定时清理这部分的内容或者不使用,目前使用最多的两种方式是

?定时任务:清理虚引用列表数据

@Component
public class CleanPhantomRefsSchedule {

    private static final ScheduledExecutorService CLEANER_EXECUTOR = Executors.newSingleThreadScheduledExecutor(r -> {
        Thread t = new Thread(r, "mysql-phantom-ref-cleaner");
        t.setDaemon(true);
        return t;
    });

    @PostConstruct
    public void doTask() {
        log.info("CleanPhantomRefsSchedule#doTask start");
        try {
            Field field = NonRegisteringDriver.class.getDeclaredField("connectionPhantomRefs");
            field.setAccessible(true);
            // 定时轮训
            CLEANER_EXECUTOR.scheduleAtFixedRate(() -> {
                try {
                    Map connectionPhantomRefs = (Map) field.get(null);
                    // 这里我设置的稍微大一些,对 gc 没太大影响时不进行干预
                   if (connectionPhantomRefs != null && connectionPhantomRefs.size() > 500) {
                        connectionPhantomRefs.clear();
                        log.info("Cleared MySQL phantom references (count={})", connectionPhantomRefs.size());
                    }
                } catch (Exception e) {
                    log.error("connectionPhantomRefs clear error!", e);
                }
            }, 1, 1, TimeUnit.HOURS);
        } catch (NoSuchFieldException e) {
            throw new IllegalStateException("Failed to initialize MySQL phantom refs field", e);
        }
    }

    @PreDestroy
    void shutdown() {
        CLEANER_EXECUTOR.shutdownNow();
    }
}

?升级 mysql-connector-java 版本(8.0 以上),可以通过参数 ??-Dcom.mysql.cj.disableAbandonedConnectionCleanup=true?? 禁用虚引用生成

 private static booleanabandonedConnectionCleanupDisabled=Boolean.getBoolean("com.mysql.cj.disableAbandonedConnectionCleanup");

 protected static void trackConnection(MysqlConnection conn, NetworkResources io) {
    if(!abandonedConnectionCleanupDisabled){
···
ConnectionFinalizerPhantomReferencereference=newConnectionFinalizerPhantomReference(conn,io,referenceQueue);
connectionFinalizerPhantomRefs.add(reference);
···
}
}

我采用的是定时任务这种方式,因为项目升级 mysql-connector-java 版本会存在兼容性问题,其次是在虚引用量比较小的情况我希望这种兜底策略依旧可以生效,所以定时任务中只有在 connectionPhantomRefs.size() > 500 时才会干预清理,而不是直接杜绝使用

验证优化结果

代码及配置调整完成后再次上线,等待一天后,继续分析 GC日志和堆转储文件

wKgZPGidVW-ALtP3AAEuzUO-pKU401.png

wKgZO2idVXCADwtKAAOCpe_-wQ0668.png

wKgZPGidVXGAU3OBAAJM7XZ0X4U432.png

结论:上线后近 12 小时数据,GC 时间最大停顿时间由原先 1.25 降低至 0.1 秒,young gc 频率由原先的 20 秒一次优化到 6 分钟一次,PhantomReference 引用耗时缩减到 0.0001966 s, 持续观察几天,线上服务无超时告警且系统响应正常,优化符合预期结果。

审核编辑 黄宇

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 接口
    +关注

    关注

    33

    文章

    9066

    浏览量

    154256
  • GC
    GC
    +关注

    关注

    0

    文章

    10

    浏览量

    17228
收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    0.1 6.0 GHz SP6T 天线开关 skyworksinc

    电子发烧友网为你提供()0.1 6.0 GHz SP6T 天线开关相关产品参数、数据手册,更有0.1 6.0 GHz SP6T 天线开关的引脚图、接线图、封装手册、中文资料、英
    发表于 08-11 18:30
    <b class='flag-5'>0.1</b> <b class='flag-5'>至</b> 6.0 GHz SP6T 天线开关 skyworksinc

    0.1 8.0 GHz SP3T 开关 skyworksinc

    电子发烧友网为你提供()0.1 8.0 GHz SP3T 开关相关产品参数、数据手册,更有0.1 8.0 GHz SP3T 开关的引脚图、接线图、封装手册、中文资料、英文资料,
    发表于 08-07 18:31
    <b class='flag-5'>0.1</b> <b class='flag-5'>至</b> 8.0 GHz SP3T 开关 skyworksinc

    0.1 3.0 GHz SP3T 开关 skyworksinc

    电子发烧友网为你提供()0.1 3.0 GHz SP3T 开关相关产品参数、数据手册,更有0.1 3.0 GHz SP3T 开关的引脚图、接线图、封装手册、中文资料、英文资料,
    发表于 08-04 18:34
    <b class='flag-5'>0.1</b> <b class='flag-5'>至</b> 3.0 GHz SP3T 开关 skyworksinc

    0.1 6.0 GHz SP3T 开关 skyworksinc

    电子发烧友网为你提供()0.1 6.0 GHz SP3T 开关相关产品参数、数据手册,更有0.1 6.0 GHz SP3T 开关的引脚图、接线图、封装手册、中文资料、英文资料,
    发表于 08-04 18:30
    <b class='flag-5'>0.1</b> <b class='flag-5'>至</b> 6.0 GHz SP3T 开关 skyworksinc

    HarmonyOS NEXT应用元服务布局优化长列表使用懒加载与组件复用

    数据如下 可以发现列表滑动时丢帧率明显降低,这是因为,List列表开启了组件复用,不会执行BuildLazyItem这个耗时操作,后续创建新组件节点时,会直接复用缓存区中的节点,这样就大幅节约了组件重新创建的时间。 本文主要引用
    发表于 06-27 16:08

    鸿蒙5开发宝藏案例分享---冷启动优化案例分享

    二次刷新 三、性能分析神器:Profiler Launch 鸿蒙DevEco Studio中的****Profiler可精准定位各阶段耗时: 通过Launch分析工具,我们发现示例中<
    发表于 06-12 17:22

    鸿蒙5开发宝藏案例分享---Web加载时延优化解析

    卡顿)。 优化核心: 减少白屏时间,提升首屏渲染速度 。 ?** 官方提供的性能分析神器** 1?? DevEco Profiler (定位耗时瓶颈) 操作路径 :DevEco Studio
    发表于 06-12 17:11

    HarmonyOS优化应用预置图片资源加载耗时问题性能优化

    降低了CPU和DDR的负载,能更快的加载图片。在Tab栏切换示例中将预置图片分别以原图(.png)、纹理超压缩(.sut)和自适应可变纹理压缩(.astc)三种方式为例,测试图片的读取耗时如下图所示
    发表于 05-29 16:11

    HarmonyOS优化应用内存占用问题性能优化

    可以避免图片过大或过小导致的显示问题,并提高应用程序的用户体验。 二、多种****方法 在日常开发中,常见的其他减少内存方式有如下几种: 使用引用(Weak Reference):在HarmonyOS
    发表于 05-21 11:27

    什么是运放的短和

    在设计运放电路的时候我们经常会接触到两个词,短和断,今天就来和大家分享下什么是运放的虛短和断。我们设运放的同相端电压为up,电流为ip,反相端电压是un,电流为in。在说短和
    的头像 发表于 05-16 19:33 ?866次阅读
    什么是运放的<b class='flag-5'>虚</b>短和<b class='flag-5'>虚</b>断

    0.1 6.0 GHz SPDT 开关 skyworksinc

    电子发烧友网为你提供()0.1 6.0 GHz SPDT 开关相关产品参数、数据手册,更有0.1 6.0 GHz SPDT 开关的引脚图、接线图、封装手册、中文资料、英文资料,
    发表于 05-13 18:30
    <b class='flag-5'>0.1</b> <b class='flag-5'>至</b> 6.0 GHz SPDT 开关 skyworksinc

    OptiSystem应用:增益平坦滤波器优化

    10次,目标是在15001600 nm范围内产生0.1 dB的增益纹波。滤波器的最小传输值为-40 dB,最大值为-0.1 dB。滤波器的传输值将在滤波器中定义的用户定义频率下进行优化
    发表于 04-10 08:49

    HarmonyOS NEXT 原生应用/元服务-性能分析基础耗时分析Time分析

    一、 函数耗时分析优化 开发应用或元服务过程中,如果遇到卡顿、加载耗时等性能问题,开发者通常会关注相关函数执行的耗时情况。DevEco Profiler提供的Time场景
    发表于 02-25 14:31

    ECRS工时分析软件如何实施精益生产??

    益生产的概念是麻省理工学院(MIT)发起的视与视ECRS工时分析软件国际汽车研究小组(IMVP)。用了5年的时间全面总结了90多家丰田汽车公司的生产方法。精益生产是相对于批量生产,它关注时间效率
    发表于 10-30 10:17

    TLV320AIC3104-Q1输入相同频率(1~5KHz)的1.2Vpp的模拟信号持续三后,输出的模拟信号幅值会自动降低,为什么?

    大家好,想请教一下,TLV320AIC3104-Q1这款芯片, 输入相同频率(1~5KHz)的1.2Vpp的模拟信号持续三后,输出的模拟信号幅值会自动降低(有点像降噪),可是没有打开这些降噪功能,为什么会有这些现象呢,规格书
    发表于 10-16 06:43