MAT分析Dump文件:
1、设置MemoryAnalyzer.ini中的-Xmx为需要用的大小,否则会遇到打开dump文件报错。。
2、dump文件导出配置:在节点配置中增加dump导出
ruby
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump/heapdump.hprof
3、dump文档加载出来后,图示如下:
核心指标解析:
-
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:组件报告,分析属于同一个根包或类加载器的对象。
可排查的问题类型:
-
内存泄漏(Memory Leak)
- 特征:内存占用持续增长,最终触发
OutOfMemoryError
。 - 常见场景:静态集合未清理、监听器未移除、
ThreadLocal
使用不当、资源未关闭(流、连接)等。
- 特征:内存占用持续增长,最终触发
-
内存溢出(OOM)
- 堆内存不足:对象过多且无法回收。
- 元空间 / 永久代溢出:类加载过多或常量池过大。
-
内存使用效率低
- 大对象频繁创建(如大字符串、大数组)导致 GC 频繁。
- 缓存设计不合理(如缓存未设置过期策略,导致对象堆积)。
-
线程相关问题
- 线程泄漏(线程创建后未销毁,持有大量资源)。
- 死锁或阻塞导致的资源无法释放。
排查流程:
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
相关类中。 -
- 关键类路径:
org.apache.shardingsphere.infra.parser.sql.SQLStatementParserEngine``org.apache.shardingsphere.infra.parser.cache.SQLStatementCache
(若存在) - 代码定位:在项目依赖中找到 ShardingSphere JDBC 的源码(或反编译 Jar 包),搜索
CacheBuilder.newBuilder()
,定位缓存创建逻辑,例如:ShardingSphere JDBC 对 SQL 解析缓存的配置,查找项目中shardingsphere-jdbc
的配置文件,是否设置了 SQL 解析缓存的参数:
- 关键类路径:
arduino
/**
* 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); // 不使用缓存:直接调用执行器解析
}
}
笔者项目中确实没有这个配置的,但默认已有大小,那为什么还会大内存,分析这个缓存存的内容有:
- Key:原始 SQL 字符串(如
"SELECT * FROM t_order WHERE id = ?"
) - Value:解析后的
SQLStatement
对象(包含表、条件、排序等结构化信息,但不包含路由结果)
以下情况会导致缓存被写入:首次执行新 SQL、缓存淘汰 / 过期后再次执行 SQL 时自动写入。那么缓存sql的大小就决定了sharding里缓存的大小,所以maximumSize的大小要去进行分析,如何配置。
5、确认是否存在sql过大,通过直方图,看到char数据的深堆大小最大,并继续查看引用链 可以看到,大对象都是查询的sql,当sql过大时,会间接导致sharding缓存过大。
因此对
maximum-sized的大小设置,要根据业务SQL特征分析:平衡SQL多样性、缓存命中率和内存占用,避免盲目配置过大(导致内存浪费)或过小(缓存失效频繁)。
SQL 多样性低(如固定 SQL 模板,参数变化但 SQL 结构不变) | 可适当增大,充分利用缓存 | 2000-5000 |
---|---|---|
SQL 多样性高(如大量动态生成 SQL,结构频繁变化) | 需减小,避免无效缓存占用内存 | 500-1000 |
高频 SQL 占比高(少数 SQL 执行次数占总流量 80% 以上) | 保证覆盖高频 SQL 即可,无需过大 | 1000-3000 |
SQL 结构复杂(单条SQLStatement 对象体积大) |
适当减小,控制总内存占用 | 500-1500 |
通过 ShardingSphere 的缓存统计(需开启sql-parser.cache.statistics-enabled: true
)和 JVM 监控,动态调整参数:
-
-
缓存命中率
- 计算公式:
命中次数 / (命中次数 + 未命中次数)
- 合理阈值:建议≥70%。若低于 50%,说明缓存利用率低,需减小
maximum-size
- 示例:1000 次查询中,800 次命中,则命中率 80%,配置合理
- 计算公式:
-
缓存实际条目数
- 若长期稳定在
maximum-size
的 80% 左右,说明配置适中 - 若长期远低于
maximum-size
(如仅使用 500 条但配置 2000),建议下调 - 若频繁达到
maximum-size
且淘汰频繁(可通过日志观察 LRU 淘汰次数),可适当上调
- 若长期稳定在
-
内存占用
- 通过 JVM 监控(如 JVisualVM)观察
SQLStatement
对象总内存 - 建议控制在 JVM 堆内存的 5%-10% 以内(如 4GB 堆内存中,缓存占用不超过 400MB)
- 通过 JVM 监控(如 JVisualVM)观察
-
-
初始配置:根据业务类型设置保守值(如 1000),并开启缓存统计
yamlyaml spring: shardingsphere: props: sql-parser.cache.maximum-size: 1000 sql-parser.cache.statistics-enabled: true
-
运行观测:收集 3-7 天的生产数据,统计:
- 系统中不同 SQL 的总数量(去重后)
- 高频 SQL(执行次数前 20%)的数量
- 缓存命中率和内存占用
-
动态调整:
- 若高频 SQL 数量为 800,可将
maximum-size
设为 1000(留 20% 冗余) - 若命中率低于 60% 且 SQL 多样性高,下调至 500
- 若内存占用过高(如单条
SQLStatement
达 10KB,1000 条即 10MB,可接受;若达 100KB,则需下调)
- 若高频 SQL 数量为 800,可将
sql
最后:针对这个内存的优化,不仅仅是排查问题后的参数优化,从参数评估到SQL都进行了优化,SQL的优化是按照一定的规范,进行问题SQL的分类,并逐一进行修改优化。具体SQL的规范和优化案例,会单独发布一篇博文