使用 MAT 分析内存溢出——Druid的stat监控及PreparedStatement缓存导致内存占用过高

236 阅读5分钟

使用 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.DruidDataSource
  • com.alibaba.druid.stat.JdbcDataSourceStat$1

分析可见,com.alibaba.druid.stat.JdbcDataSourceStat 这个类占用了大量内存。

2. Dominator Tree(支配树)分析

Dominator Tree 视图中,可以看到 com.alibaba.druid.pool.DruidDataSource 是占用内存最大的对象。

dominator_tree1.png

进一步展开后,发现其内部有一个 HashMap 对象,存储了大量 SQL 语句。

dominator_tree1_druid.png

分析结论: 该问题主要是由于 Druid 的 stat 监控功能记录了大量 SQL 语句,而我们的测试场景中 IN 条件中的占位符非常多,达到了 2 万多个,导致内存溢出。

解决方案

  • 关闭 Druid 的 stat 监控,或者
  • 限制 SQL 记录数量。

我们最终选择关闭 stat 监控。

3. Histogram(直方图)分析

Histogram 视图中,发现占用内存最多的是 char[] 对象。

操作步骤

  1. 右键 char[] -> List Objects -> with Incoming References
  2. 发现很多 1MB 以上的 SQL 语句,主要指向 com.alibaba.druid.stat.JdbcSqlStat 对象。

histogram1.png

histogram1_char.png

虽然这些 SQL 语句非常大,但这并不是导致本次内存溢出的主要原因,因此暂时忽略。


第二次内存溢出

关闭 druidstat 监控后,我们重新进行测试,结果仍然发生了 内存溢出

1. Leak Suspects(内存泄露嫌疑点)视图

这次的泄露点与上一次不同:

Leak_Suspects2.png

2. Dominator Tree(支配树)分析

ClientPreparedStatement对象分析

Dominator Tree 视图中,发现 ClientPreparedStatement 对象占用了大量内存,而这些对象主要存储的是 SQL 语句。

dominator_tree2.png

进一步分析 SQL 语句后发现,大量 IN 条件不同的 SQL 语句被缓存,导致 PreparedStatement 对象占用内存过高。

dominator_tree2_preparedstatement.png

thread对象分析

查看 thread 对象,找到占用内存较高的 SQL,发现主要是 Flowable 任务查询时,processInstanceIdIn() 传入过多参数,导致单次查询返回了 2 万条数据

dominator_tree2_thread_list.png

dominator_tree2_thread_boundSql.png

4. 代码分析及最终解决方案

综合分析后,发现存在两个主要问题:

  1. Druid 缓存了大量 PreparedStatement 对象,由于 SQL 语句 IN 参数个数不同,导致缓存了多个不同的 PreparedStatement
  2. SQL 查询返回数据过多,单次查询返回 2 万条数据,导致大量内存被占用。

解决方案

  • 限制 IN 参数的个数:固定每次最多 2000 个,然后分批查询(减少 PreparedStatement 的个数和大小)。
  • 优化 taskService.createTaskQuery().processInstanceIdIn() 查询:改为自定义查询方式,减少返回的结果集大小。

问题验证

1. Histogram(直方图)验证

优化后,重新进行测试,在 char[] 的 Inbound 视图 中,已经没有指向 druid-stat 的对象,大于 1MB 的字符串也明显减少,说明 stat 监控的问题已经解决

histogram2.png

histogram2_char.png

2. Object[] 分析

Object[]Outbound 视图 中,很多大对象主要集中在 internalRowData,基本是数据库查询的结果集,可以辅助推断是哪个 SQL 造成的。

histogram2_object.png

histogram2_object_data.png


总结

本次内存溢出问题主要由 Druid stat 监控及 PreparedStatement 缓存导致,通过 关闭 stat 监控、优化 SQL 查询及限制 IN 参数个数,最终解决了问题。