使用 MAT 分析内存溢出——Druid的stat监控及PreparedStatement缓存导致内存占用过高
最近测试环境发生了内存溢出问题。为了提前发现问题,我们在测试环境中将堆内存限制为 -Xmx=512M,并在启动脚本中添加了 -XX:+HeapDumpOnOutOfMemoryError,这样当发生内存溢出时,JVM 会自动生成堆转储(Heap Dump)文件。
MAT 关键概念
在使用 Memory Analyzer Tool (MAT) 进行分析时,以下几个参数非常重要:
- Shallow Heap(浅堆):指对象本身占用的内存(不包括其引用的对象)。
- Retained Heap(深堆):某对象被保留时,其依赖的所有对象的浅堆之和。
- Outgoing References(外部引用):当前对象所引用的外部对象集合。
- Incoming References(被引用对象):引用当前对象的外部对象集合。
第一次内存溢出
1. Leak Suspects(内存泄露嫌疑点)视图
在 MAT 的 Leak Suspects 视图中,发现了以下关键信息:
One instance of "com.alibaba.druid.pool.DruidDataSource" loaded by
"org.springframework.boot.loader.LaunchedURLClassLoader @ 0xe02f3600"
occupies 167.59 MB (36.82%) bytes.
The memory is accumulated in one instance of
"com.alibaba.druid.stat.JdbcDataSourceStat$1"
loaded by "org.springframework.boot.loader.LaunchedURLClassLoader @ 0xe02f3600".
关键词:
com.alibaba.druid.pool.DruidDataSourcecom.alibaba.druid.stat.JdbcDataSourceStat$1
分析可见,com.alibaba.druid.stat.JdbcDataSourceStat 这个类占用了大量内存。
2. Dominator Tree(支配树)分析
在 Dominator Tree 视图中,可以看到 com.alibaba.druid.pool.DruidDataSource 是占用内存最大的对象。
进一步展开后,发现其内部有一个 HashMap 对象,存储了大量 SQL 语句。
分析结论:
该问题主要是由于 Druid 的 stat 监控功能记录了大量 SQL 语句,而我们的测试场景中 IN 条件中的占位符非常多,达到了 2 万多个,导致内存溢出。
解决方案:
- 关闭 Druid 的
stat监控,或者 - 限制 SQL 记录数量。
我们最终选择关闭 stat 监控。
3. Histogram(直方图)分析
在 Histogram 视图中,发现占用内存最多的是 char[] 对象。
操作步骤:
- 右键 char[] -> List Objects -> with Incoming References
- 发现很多 1MB 以上的 SQL 语句,主要指向
com.alibaba.druid.stat.JdbcSqlStat对象。
虽然这些 SQL 语句非常大,但这并不是导致本次内存溢出的主要原因,因此暂时忽略。
第二次内存溢出
关闭 druid 的 stat 监控后,我们重新进行测试,结果仍然发生了 内存溢出。
1. Leak Suspects(内存泄露嫌疑点)视图
这次的泄露点与上一次不同:
2. Dominator Tree(支配树)分析
ClientPreparedStatement对象分析
在 Dominator Tree 视图中,发现 ClientPreparedStatement 对象占用了大量内存,而这些对象主要存储的是 SQL 语句。
进一步分析 SQL 语句后发现,大量 IN 条件不同的 SQL 语句被缓存,导致 PreparedStatement 对象占用内存过高。
thread对象分析
查看 thread 对象,找到占用内存较高的 SQL,发现主要是 Flowable 任务查询时,processInstanceIdIn() 传入过多参数,导致单次查询返回了 2 万条数据。
4. 代码分析及最终解决方案
综合分析后,发现存在两个主要问题:
- Druid 缓存了大量
PreparedStatement对象,由于 SQL 语句IN参数个数不同,导致缓存了多个不同的PreparedStatement。 - SQL 查询返回数据过多,单次查询返回 2 万条数据,导致大量内存被占用。
解决方案:
- 限制
IN参数的个数:固定每次最多 2000 个,然后分批查询(减少PreparedStatement的个数和大小)。 - 优化
taskService.createTaskQuery().processInstanceIdIn()查询:改为自定义查询方式,减少返回的结果集大小。
问题验证
1. Histogram(直方图)验证
优化后,重新进行测试,在 char[] 的 Inbound 视图 中,已经没有指向 druid-stat 的对象,大于 1MB 的字符串也明显减少,说明 stat 监控的问题已经解决。
2. Object[] 分析
在 Object[] 的 Outbound 视图 中,很多大对象主要集中在 internalRowData,基本是数据库查询的结果集,可以辅助推断是哪个 SQL 造成的。
总结
本次内存溢出问题主要由 Druid stat 监控及 PreparedStatement 缓存导致,通过 关闭 stat 监控、优化 SQL 查询及限制 IN 参数个数,最终解决了问题。