Java堆转储分析:定位io.netty.buffer.ByteBufUtil等占内存对象解决内存泄漏
堆转储精准定位内存泄漏实操步骤
- 优先通过Eclipse Memory Analyzer(MAT)生成内存泄漏嫌疑报告,不要仅查看大对象列表:右键选中待排查的大对象,选择
Path to GC Roots > exclude all phantom/weak/soft etc. references查看完整引用链,即可定位到业务代码/第三方依赖中持有该对象的上层调用方,解决你找不到对应代码的问题。 - 针对未显式引入的Netty依赖,执行依赖树命令扫全量隐式依赖:Maven项目执行
mvn dependency:tree,Gradle项目执行gradle dependencies。Spring Boot生态中很多常用Starter会默认引入Netty,包括Spring Cloud Gateway、AWS SDK v2、Redis/Elasticsearch客户端、Webflux依赖等,全局检索业务代码自然无法命中。 - 针对找不到归属的
Byte[]和LinkedHashMap对象,在MAT中查看对象的outgoing references,可以查看数组内部存储的具体内容,比如HTTP请求报文、数据库查询结果、第三方组件缓存数据等,根据内容反推对应归属组件。
已识别大对象的针对性解决方案
io.netty.buffer.ByteBufUtil.DEFAULT_ALLOCATOR 32MB占用问题
该对象是Netty默认的字节缓冲分配器,异常高占用通常由缓冲泄漏、池化内存配置不合理导致:
- 首先将Netty升级到对应大版本的最新稳定版,4.1.x版本低于4.1.68.Final存在多个已知的ByteBuf泄漏官方bug,升级可直接解决这类已知问题。
- 启动参数新增
-Dio.netty.leakDetection.level=PARANOID开启全量泄漏检测,运行1~2天后日志会打印泄漏ByteBuf的完整分配栈,直接定位到调用方代码。 - 对于QPS不高的普通Rest API服务,可直接调整启动参数降低池化内存占用:
- 降低池化内存块上限:
-Dio.netty.allocator.maxOrder=9,将默认16MB的最大池化块降到4MB - 切换为非池化分配器:
-Dio.netty.allocator.type=unpooled,对低QPS服务的性能影响可以忽略,可直接降低几十MB的Netty常驻内存占用。
- 降低池化内存块上限:
com.mysql.cj.jdbc.AbandonedConnectionCleanupThread.connectionFinalizerPhantomRefs 20MB占用问题
该对象是MySQL JDBC驱动持有的废弃连接虚引用集合,高占用说明存在大量未正常关闭的数据库连接堆积:
- 检查数据库连接池配置:以HikariCP为例,
maxLifetime参数要比MySQL服务端的wait_timeout参数短至少30秒,避免拿到已被MySQL服务端主动断开的无效连接,这类无效连接会进入虚引用集合等待清理,清理不及时就会持续堆积。 - 排查业务代码中手动获取Connection的逻辑,必须通过try-with-resources语法自动释放连接,或者在finally块中手动调用close方法,避免连接泄漏。
- 升级
mysql-connector-java到8.0.30以上版本,该版本修复了AbandonedConnectionCleanupThread清理线程阻塞、虚引用无法释放的官方bug。
验证与临时缓解方案
- JVM启动参数新增
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,OOM发生时自动生成堆转储,比手动执行jmap抓取的快照更贴合泄漏现场。 - 开启GC日志:
-Xlog:gc*:file=/tmp/gc.log:time,uptime:filecount=10,filesize=100M,观察Old区内存增长趋势:如果每次Full GC后Old区的最低占用值持续升高,即可确认存在内存泄漏。 - 修复上线后持续观测7天内存变化,正常修复后内存日增长量会控制在5MB以内,Full GC后内存可回落至初始占用的10%浮动区间。
内容的提问来源于stack exchange,提问作者Kishan Solanki
相关产品推荐
相关产品推荐

