MAT分析内存溢出- ShardingSphere JDBC的缓存泄露问题
MAT分析Dump文件:
1、设置MemoryAnalyzer.ini中的-Xmx为需要用的大小,否则会遇到打开dump文件报错。。
2、dump文件导出配置:在节点配置中增加dump导出
3、dump文档加载出来后,图示如下:

核心指标解析:
1、定位可疑区域:查看报告中的Leak Suspects,每个Suspect会【显示可疑对象占用内存比例】;重点看Problem Suspect 1(最可能的泄漏点),记录其对象类型(如

解析:
Keywords:关键信息,列出了关键的类和类加载器等标识,方便定位和关联代码及相关组件,
2、分析details:

解析:
3、跟踪引用链

关键路径:
内存占比验证:
以上只是为了从不同的区域去看,同样也可以从直方图去看,根据Retained Heap降序,找出top对象信息,并看引用链。
4、结合业务代码分析:
1、设置MemoryAnalyzer.ini中的-Xmx为需要用的大小,否则会遇到打开dump文件报错。。
2、dump文件导出配置:在节点配置中增加dump导出
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump/heapdump.hprof

核心指标解析:
- Histogram:直方图,列出每个类的实例数量
- Objects:对象个数
- Shallow Heap:浅堆,表示对象占用多少内存。
- Retained Heap:深堆,表示对象依赖的底层所有对象的总内存。
- with outgoing references:此对象引用了哪些对象
- with incoming references:此对象被谁引用
- Dominator Tree:支配树,列出最大的对象以及它们使哪些对象保持存活。
- Top Consumers:按类和包对最占用资源的对象进行分组打印。
- Duplicate Classes:检测由多个类加载器加载的类。
- Leak Suspects:怀疑内存泄露
- Top Components:列出占堆总大小超过 1% 的组件报告。
- Component Report:组件报告,分析属于同一个根包或类加载器的对象。
1、定位可疑区域:查看报告中的Leak Suspects,每个Suspect会【显示可疑对象占用内存比例】;重点看Problem Suspect 1(最可能的泄漏点),记录其对象类型(如
java.util.HashMap)和支配树路径(如MyCache → HashMap → Entry[])。
解析:
org.springframework.boot.loader.LaunchedURLClassLoader @0x4c0005138 是一个类加载器,占用了约 2,014,349,632 字节(占比 67.97% ),这些内存主要是因为加载了 com.google.common.cache.LocalCache$Segment[] 这个实例导致的堆积。可能存在内存泄漏或者该部分对象占用内存过大的情况,需要进一步结合代码里对 Guava Cache(即 com.google.common.cache 相关)的使用逻辑,比如缓存对象是否没有合理失效、缓存配置的容量是否过大等,来排查为何会出现这么高的内存占用 。Keywords:关键信息,列出了关键的类和类加载器等标识,方便定位和关联代码及相关组件,
com.google.common.cache.LocalCache$Segment[] 表明是 Guava 缓存的段数组相关,org.springframework.boot.loader.LaunchedURLClassLoader @0x4c0005138 是加载这些类的类加载器实例标识,可辅助在代码和类加载机制层面去深挖问题根源 。点击 Details 通常能查看更详细的对象引用链、内存占用细节等,助力进一步分析内存问题。2、分析details:

解析:
- 从引用链看
LocalCache→loadingCache→sqlStatementCache,说明是 ShardingSphere 的 SQL 语句解析缓存(SQLStatementParserEngine相关) 在占用内存。 - 逐层展开后,最终关联到
org.springframework.boot.loader.LaunchedURLClassLoader加载的类(classes节点),体现了类加载器持有缓存对象,导致内存无法释放 。 - 下方多个线程(如 Log4j 线程、Tomcat 线程、ShardingSphere 元数据加载线程等)的
contextClassLoader都指向LaunchedURLClassLoader,说明 这些线程在运行时依赖该类加载器加载的类 。若线程未正确终止或类加载器未被释放,会进一步延长缓存对象的生命周期,加剧内存占用。
SQLStatementParserEngine 相关的内存占用非常大,3、跟踪引用链
with outgoing references
关键路径:
SQLStatementParserEngineFactory → ENGINES(ConcurrentHashMap) → SQLStatementParserEngine → sqlStatementCache → LocalCache → segments(LocalCache$Segment[])。这是 ShardingSphere SQL 解析缓存的完整引用链,segments 数组是内存堆积的直接载体(对应最初的 LocalCache$Segment[] 占用问题)。内存占比验证:
LocalCache 的 Retained Heap 高达 1,878,763,216(约 1.75GB),说明缓存对象确实在此大量堆积。以上只是为了从不同的区域去看,同样也可以从直方图去看,根据Retained Heap降序,找出top对象信息,并看引用链。
4、结合业务代码分析:
- 组件关联:所有内存堆积都关联
SQLStatementParserEngine(ShardingSphere 负责 SQL 解析缓存的核心类)和LocalCache(Guava Cache 实现),且SQLStatementParserEngine是 ShardingSphere JDBC 的内置组件,说明缓存逻辑由 ShardingSphere 触发。 - 缓存特性:ShardingSphere JDBC 默认会缓存 SQL 解析结果(通过
SQLStatementCache),若未合理配置缓存容量 / 过期时间,或业务场景中SQL 多样性极高(如动态参数 SQL 过多),就会导致缓存无限堆积,符合 “缓存泄漏” 特征(对象无法被 GC 回收,内存持续增长)。 - 找到ShardingSphere 缓存配置的核心类,ShardingSphere JDBC 的 SQL 解析缓存由
SQLStatementParserEngine管理,其缓存初始化逻辑在SQLStatementParserEngineFactory或SQLStatementCache相关类中。 -
笔者项目中确实没有这个配置的,但默认已有大小,那为什么还会大内存,分析这个缓存存的内容有:
/** * SQL语句解析引擎,负责SQL语句的解析和缓存管理 * 核心作用是将原始SQL字符串解析为结构化的SQLStatement对象,同时提供缓存机制提升性能 */ public final class SQLStatementParserEngine { /** * SQL语句解析执行器,实际执行SQL解析的组件 * 封装了不同数据库类型的SQL解析逻辑 */ private final SQLStatementParserExecutor sqlStatementParserExecutor; /** * SQL语句缓存,使用LoadingCache实现(通常是Guava的缓存实现) * 键为SQL字符串,值为解析后的SQLStatement对象 * 具备自动加载和过期淘汰能力 */ private final LoadingCache<String, SQLStatement> sqlStatementCache; /** * 构造方法,初始化解析引擎 * * @param databaseType 数据库类型(如MySQL、PostgreSQL等) * @param sqlCommentParseEnabled 是否解析SQL中的注释 */ public SQLStatementParserEngine(String databaseType, boolean sqlCommentParseEnabled) { // 初始化解析执行器,传入数据库类型和注释解析开关 this.sqlStatementParserExecutor = new SQLStatementParserExecutor(databaseType, sqlCommentParseEnabled); // 构建SQL语句缓存 // CacheOption参数说明: // 2000:缓存最大条目数(maximumSize) // 65535L:缓存过期时间(expireAfterWrite,单位根据实现可能为秒或毫秒) // 4:可能是并发级别(concurrencyLevel),允许同时写入缓存的线程数 this.sqlStatementCache = SQLStatementCacheBuilder.build( new CacheOption(2000, 65535L, 4), databaseType, sqlCommentParseEnabled ); } /** * 解析SQL语句的核心方法 * * @param sql 原始SQL字符串 * @param useCache 是否使用缓存:true-优先从缓存获取,未命中则解析并缓存;false-直接解析不使用缓存 * @return 解析后的SQLStatement对象(包含SQL结构化信息,如表名、条件、排序等) */ public SQLStatement parse(String sql, boolean useCache) { // 根据useCache参数决定是否使用缓存 return useCache ? (SQLStatement)this.sqlStatementCache.getUnchecked(sql) // 使用缓存:从缓存获取,无则自动加载 : this.sqlStatementParserExecutor.parse(sql); // 不使用缓存:直接调用执行器解析 } }
本文来自博客园,作者:难得,转载请注明原文链接:https://www.cnblogs.com/zhangbLearn/p/18996179

可以看到,大对象都是查询的sql,当sql过大时,会间接导致sharding缓存过大。
因此对
浙公网安备 33010602011771号