找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4845

积分

0

好友

663

主题
发表于 2 小时前 | 查看: 2| 回复: 0

背景

早上 9 点 34 分,系统监控发出 JVM 内存占用过高的报警。

JVM堆内存使用率过高告警截图

登录 Grafana 查看系统占用情况,发现从 9 点 35 分开始,没有任何业务进程在执行,但内存占用率居高不下,持续观察几分钟后情况依然没有变化。

系统性能监控图表,显示高堆内存使用率

开始排查

立刻登录服务器,开始对 JVM 内存的详细占用情况进行排查。

1. 查看内存中对象直方图

首先,使用 jmap -histo 命令快速查看堆内存中哪些对象占用了大量空间。

[jenkins@jt-kaifa-app ~]$ ps -ef | grep java  # 查看8881 java进程号

[jenkins@jt-kaifa-app ~]$ cd /data/tools/jdk21/bin  # cd 到 jdk bin 目录
[jenkins@jt-kaifa-app bin]$ ls
jar        java   javadoc  jcmd      jdb        jdeps  jhsdb   jinfo  jmap  jpackage  jrunscript  jstack  jstatd      keytool      serialver
jarsigner  javac  javap    jconsole  jdeprscan  jfr    jimage  jlink  jmod  jps       jshell      jstat   jwebserver  rmiregistry

[jenkins@jt-kaifa-app bin]$ sudo ./jmap -histo 15848 | head -30
 num     #instances         #bytes  class name (module)
-------------------------------------------------------
   1:      10757711     1191171128  [Ljava.lang.Object; (java.base@21.0.5)
   2:       1231953      877362976  [I (java.base@21.0.5)
   3:        378839      713984048  [Ljdk.internal.vm.FillerElement; (java.base@21.0.5)
   4:      19113922      611645504  java.util.HashMap$Node (java.base@21.0.5)
   5:      10574310      575414680  [J (java.base@21.0.5)
   6:       4921585      504043712  [B (java.base@21.0.5)
   7:      17108100      410594400  com.fasterxml.jackson.databind.type.ClassKey
   8:       4731705      339114256  [Ljava.util.HashMap$Node; (java.base@21.0.5)
   9:       1369285      324619496  [Ljava.lang.String; (java.base@21.0.5)
  10:       4570927      292539328  java.util.concurrent.ConcurrentHashMap (java.base@21.0.5)
  11:       3421835      273746800  com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap
  12:       3954296      221440576  java.nio.HeapCharBuffer (java.base@21.0.5)
  13:       4133491      198407568  java.util.HashMap (java.base@21.0.5)
  14:       6939909      166557816  java.util.concurrent.atomic.AtomicLong (java.base@21.0.5)
  15:       4093315      156108688  [C (java.base@21.0.5)
  16:       6230101      149522424  java.util.ArrayList (java.base@21.0.5)
  17:       3599029      143961160  java.util.LinkedHashMap$Entry (java.base@21.0.5)
  18:       4483728      143479296  java.util.concurrent.locks.ReentrantLock$NonfairSync (java.base@21.0.5)
  19:       6843688      109499008  java.util.concurrent.atomic.AtomicLongArray (java.base@21.0.5)
  20:       3328158      106501056  org.springframework.http.MediaType
  21:       1265477       91114344  java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask (java.base@21.0.5)
  22:       3732805       89587320  java.lang.String (java.base@21.0.5)
  23:       1376234       88078976  java.util.LinkedHashMap (java.base@21.0.5)
  24:       3612213       86693112  java.util.concurrent.ConcurrentLinkedQueue$Node (java.base@21.0.5)
  25:       3423544       82165056  java.util.concurrent.ConcurrentLinkedQueue (java.base@21.0.5)
  26:       3421835       82124040  com.fasterxml.jackson.databind.util.LRUMap
  27:       3421835       82124040  com.fasterxml.jackson.databind.util.internal.LinkedDeque
  28:       4854878       77678048  java.util.concurrent.atomic.AtomicReference (java.base@21.0.5)

含义:查看内存中对象的直方图(统计前 30 行)

  • jmap -histo: 生成 Java 堆中对象的统计信息。它列出了每种特定类型的对象数量及所占用的字节数。
  • 作用: 快速判断哪些类占据了大部分内存。
  • 分析输出:
    • [Ljava.lang.Object;: 这是对象数组,占用内存最高(约 1.19 GB)。
    • java.util.HashMap$Node: HashMap 的节点,数量高达 1900 万个。
    • com.fasterxml.jackson.databind.type.ClassKey: 这是 Jackson(JSON 处理库)相关的类。
  • 关键点: Jackson 相关的类(如 ClassKey, LRUMap, PrivateMaxEntriesMap)频繁出现且数量巨大(百万级)。这通常暗示 Jackson 的类型缓存或对象映射器(ObjectMapper)配置不当,导致了严重的内存积压。

2. 监控垃圾回收

接着,使用 jstat 命令查看实时的垃圾回收状态。

[jenkins@jt-kaifa-app bin]$ sudo ./jstat -gcutil 15848 1000 10
  S0     S1     E      O      M     CCS    YGC     YGCT     FGC    FGCT     CGC    CGCT       GCT   
     -  97.08  30.48  94.02  98.99  95.65   2364    51.352     0     0.000  1446     6.743    58.095
     -  97.08  30.65  94.02  98.99  95.65   2364    51.352     0     0.000  1446     6.743    58.095
     -  97.08  30.81  94.02  98.99  95.65   2364    51.352     0     0.000  1446     6.743    58.095
     -  97.08  30.81  94.02  98.99  95.65   2364    51.352     0     0.000  1446     6.743    58.095
     -  97.08  30.81  94.02  98.99  95.65   2364    51.352     0     0.000  1446     6.743    58.095
     -  97.08  30.81  94.02  98.99  95.65   2364    51.352     0     0.000  1446     6.743    58.095
     -  97.08  30.97  94.02  98.99  95.65   2364    51.352     0     0.000  1446     6.743    58.095
     -  97.08  31.13  94.02  98.99  95.65   2364    51.352     0     0.000  1446     6.743    58.095
     -  97.08  31.13  94.02  98.99  95.65   2364    51.352     0     0.000  1446     6.743    58.095
     -  97.08  31.29  94.02  98.99  95.65   2364    51.352     0     0.000  1446     6.743    58.095

含义:每秒监控一次垃圾回收(GC)状态,共执行 10 次

  • jstat -gcutil: 总结垃圾回收统计信息。
  • 参数: 1000 表示采样间隔 1000ms(1秒),10 表示采样 10 次。
  • 列含义:
    • S1: 幸存者 1 区占用 97.08%,非常满。
    • E: 伊甸园区(Eden)占用约 31%,正在缓慢增长。
    • O (Old): 老年代占用 94.02%。这是一个危险信号,说明大部分内存已经被占满,接近触发 Full GC 的边缘。
    • M / CCS: 元空间占用约 98% / 95%,说明加载的类非常多。
    • YGC: 已发生 2364 次轻量级 GC。
    • CGC / CGCT: 现代垃圾回收器(如 G1 或 ZGC)的并发回收次数和时间。
  • 结论: 内存压力极大,老年代几乎填满,系统可能存在内存泄漏或堆空间分配不足。

3. 统计线程总数

[jenkins@jt-kaifa-app bin]$ ps -Lf 15848 | wc -l
314

含义:统计该进程下的线程总数

  • ps -Lf: 列出指定进程的所有 LWP(轻量级进程,即线程)。
  • wc -l: 统计行数。
  • 分析: 输出 314。对于一个中大型 Spring Boot 或微服务应用,314 个线程属于正常范围,目前看不出明显的线程泄露。

4. 转储Dump文件

为了进行更深入的分析,需要生成一个完整的堆转储文件。

[jenkins@jt-kaifa-app bin]$ sudo ./jmap -dump:live,format=b,file=8881heap.hprof 15848
Dumping heap to /data/tools/jdk21/bin/8881heap.hprof ...
Heap dump file created [14360374537 bytes in 52.589 secs]

含义:导出内存堆转储(Heap Dump)文件

  • -dump:live: 只导出存活的对象(在导出前会强制触发一次 Full GC,剔除可回收对象)。
  • format=b: 以二进制格式导出。
  • file=8881heap.hprof: 指定生成的文件名。
  • 分析: 文件大小为 14,360,374,537 字节(约 13.3 GB)

5. 使用JProfiler深度分析

将生成的 hprof 文件下载到本地,使用专业的 Java 性能分析工具 JProfiler(可从 https://www.ej-technologies.com/jprofiler/download15 下载)进行深度分析。

JProfiler下载页面截图

使用激活码 S-J15-NEO_PENG#890808-1a6eo5gvl1w9v#b6bab 激活后,打开转储文件。

JProfiler打开的hprof文件,显示对象统计

从概览中看到 java.util.HashMap$Nodecom.fasterxml.jackson.databind.type.ClassKey 数量异常庞大。右键点击 HashMap$Node,选择“使用选定对象”进行进一步分析。

JProfiler中右键选择使用选定对象
JProfiler创建新对象集对话框

分析结果揭示了对象的引用链。

JProfiler显示HashMap$Node对象的引用链
JProfiler显示对象引用树状结构

6. 初步定位与AI辅助分析

将 JProfiler 的分析截图提交给 AI 工具,请求其作为资深 Java 工程师帮助分析。

请求AI分析JProfiler文件的对话截图

AI 分析后给出了初步诊断,指出问题很可能与 WebClient 的滥用、连接池泄漏或响应式流未终止有关。

AI分析报告,指出WebClient相关的高频问题场景

7. 根因确认

根据 AI 的指向,检查相关代码。发现应用中的一个 ApiClient 类,为了实现动态超时配置,在 setTimeOut 方法中,一旦连接或响应超时参数发生变化,就会重新创建 WebClient 实例。

AI 进一步解释了这种做法的危害:频繁创建新的 WebClient 会导致旧的配置对象(包含 Lambda、ExchangeStrategies 和 Jackson 的 ObjectMapper)被底层 Netty 连接池“绑架”而无法释放,最终引发内存泄漏。

/**
 * 同时设置连接超时和响应超时(立即生效)
 *
 * @param connectTimeoutSeconds 连接超时秒数
 * @param responseTimeoutSeconds 响应超时秒数
 */
public void setTimeOut(int connectTimeoutSeconds, int responseTimeoutSeconds) {
    Duration newConnectTimeout = Duration.ofSeconds(connectTimeoutSeconds);
    Duration newResponseTimeout = Duration.ofSeconds(responseTimeoutSeconds);
    // 只有当超时参数发生变化时才重新创建WebClient
    if (!this.connectTimeout.equals(newConnectTimeout) || !this.responseTimeout.equals(newResponseTimeout)) {
        this.connectTimeout = newConnectTimeout;
        this.responseTimeout = newResponseTimeout;
        this.webClient = createWebClient(); // 重新创建WebClient使配置生效
    }
}

AI分析WebClient动态创建导致内存泄漏的原理
AI分析内存泄露完整逻辑链条

修改方案

定位到根本原因后,对代码进行如下重构:

  1. 不再动态重建 WebClient
    移除/停用 setConnectTimeoutsetResponseTimeoutsetTimeout 这类会触发 createWebClient() 的方法,避免不断创建新的 ExchangeStrategies / ObjectMapper 链。

  2. 连接超时固定化
    CONNECT_TIMEOUT_MILLIS 使用固定默认值(如 5s)在初始化时配置一次。连接超时属于 Netty 连接建立阶段参数,不适合按请求频繁动态切换。

  3. 响应超时请求级可传入
    get / post / put / json 等方法中增加带 Duration responseTimeout 参数的重载版本。在内部请求执行逻辑中,使用 Mono.timeout(effectiveTimeout) 来实现单次请求级别的超时控制。

  4. 共享编解码策略
    ExchangeStrategies 提升为 static final(例如 SHARED_STRATEGIES),这样整个应用复用同一套 Jackson 编解码器,从根本上避免 ObjectMapper 相关缓存对象的重复膨胀。

通过以上优化,系统内存使用迅速恢复正常。这个案例提醒我们,在使用 Spring WebClient 这类响应式客户端时,需要特别注意其生命周期与底层连接池的关系,避免因配置的动态变更导致不可预料的内存问题。


本文所涉及的 JVM 内存泄漏排查思路和分析工具,对于深入理解 Java 应用的运行时行为非常有帮助。如果你想了解更多类似的系统级诊断与架构设计实践,欢迎关注 云栈社区后端 & 架构 板块,那里有更多来自一线工程师的深度分享。




上一篇:云原生CMS新选择:EmDash用Worker沙箱重构插件安全,与Astro深度集成
下一篇:AI治理之争:Anthropic拒绝武器化引发企业自治与政府监管冲突
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-9 06:50 , Processed in 0.602826 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表