祖传诊断工具: VisualVm
分析原因
这里用的一个小型项目(49张表),用代码生成工具生成了代码,注入默认SQL方法,也就是 49 * 16 = 784 个 MappedStatement
通过dump内存分析得出,Mybatis占用内过多的MappedStatement和DynamicSqlSource以及一些SqlNode相关的
这里说说MappedStatement与sqlSource是怎么来的,我们来看一个Mybatis的动态SQL小示例,
<!--摘取至mybatis官网示例-->
<select id="findActiveBlogLike" resultType="Blog">
<!--StaticTextSqlNode-->
SELECT * FROM BLOG
<!--WhereNode-->
<where>
<!--IfSqlNode-->
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
<!--StaticTextSqlNode-->
AND title like #{title}
</if>
<if test="author != null and author.name != null">
<!--StaticTextSqlNode-->
AND author_name like #{author.name}
</if>
</where>
</select>
Mybatis启动会解析mapper的xml文件中的<select><update><delete><insert>节点来构建MappedStatement,一个MappedStatement就是对应我们Mapper中的一个方法,那么成员属性sqlSource就好理解了,就是我们上述节点生成的内容构建一个sqlSource,根节点为MixedSqlNode,这样一直包含下面的节点解析下去.
从上面可以看出,MappedStatement之所以占用堆内存大的原因是因为sqlSource属性持有的SqlNode占用过多,主要在IfSqlNode和StaticTextSqlNode生成过多.
而这两个节点里面的内容其实就是我们xml里面写的文本(这里截取的实际项目里的,和上面示例的不一样),
IfSqlNode存储的为if标签中写test属性
StaticTextSqlNode存储的为我们写的SQL内容
下面我们再看两张图 (注意看两个String的内存地址)
我们从上面的图片可以看出,Mybatis解析出来的String相同的内容内存地址是不一样的,这个也就是为什么使用Mybatis-Plus注入一些通用SQL导致堆内存占用过多的原因,既然原因分析出来了,那我们如果把这些重复的字符串intern进字符串常量池或者把这块重复的字符串在堆里只保留一份,是不是就能把这些内存空间省下来了.
实战阶段,准备动手解决.
我们将XMLScriptBuilder的代码复制出来做一些改动,这个最好选取当前项目使用的Mybatis版本的源码来修改(我用3.3.2的源码配套的Mybatis版本ForEachHandler是没有nullable属性的).
下面示例代码只是展示了一些修改地方,完整源码参考: github.com/baomidou/my…
/**
* <p>试验性功能,解决mybatis堆内存过大的问题(看后期mybatis官方会不会解决堆内存占用问题)</p>
* <p>由于大量重复sql节点,导致堆内存过大(本质上属于string导致的堆内存增大问题)</p>
* <p>例如: {@code <if test="createTime!=null">create_time=#{createTime}</if>}等公共字段</p>
* <p>
* 解决方案: 将生成的xml节点值写入字符串常量池,减少后面重复字符串导致的问题
* <li>
* 方案一: 缓存一些特定的mybatis-plus生成的占位符与表达式和项目公共字段(改动有点多,需要增加一些特定xml属性来标记是mybatis-plus生成的节点,减少堆内存较少)
* </li>
* <li>
* 方案二: 直接将节点内容intern写入至字符串常量池(改动少,减少堆内存多,弊端可能会将一些无重复的字符串写入至常量池)
* </li>
* <li>
* 方案三: 模拟字符串常量池,减少重复字符串写入至堆内存(代码相对来说不好看点)
* </li>
* </p>
*
* @author nieqiurong
* @see XMLScriptBuilder
* @since 3.5.3
*/
public class MybatisXMLScriptBuilder extends BaseBuilder {
private final XNode context;
private boolean isDynamic;
private final Class<?> parameterType;
private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();
//这里是增加的
private static final Map<String, WeakReference<String>> CACHE_STRING = new WeakHashMap<>();
/**
* 也可以将XNode节点包裹增强一下,来减少方法的引用,但需要创建对象,这里就直接将每个地方手动改一下了.
*/
//这里是增加的,其他地方调用这里的不再继续做说明
private synchronized static String cacheStr(String str) {
if (str == null) {
return null;
}
return CACHE_STRING.computeIfAbsent(str, WeakReference::new).get();
}
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String text = cacheStr(child.getStringBody(""));
TextSqlNode textSqlNode = new TextSqlNode(text);
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(text));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
private class BindHandler implements NodeHandler {
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
final String name = cacheStr(nodeToHandle.getStringAttribute("name"));
final String expression = cacheStr(nodeToHandle.getStringAttribute("value"));
final VarDeclSqlNode node = new VarDeclSqlNode(name, expression);
targetContents.add(node);
}
}
private class TrimHandler implements NodeHandler {
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
String prefix = cacheStr(nodeToHandle.getStringAttribute("prefix"));
String prefixOverrides = cacheStr(nodeToHandle.getStringAttribute("prefixOverrides"));
String suffix = cacheStr(nodeToHandle.getStringAttribute("suffix"));
String suffixOverrides = cacheStr(nodeToHandle.getStringAttribute("suffixOverrides"));
TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
targetContents.add(trim);
}
}
private class ForEachHandler implements NodeHandler {
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
String collection = cacheStr(nodeToHandle.getStringAttribute("collection"));
Boolean nullable = nodeToHandle.getBooleanAttribute("nullable");
String item = cacheStr(nodeToHandle.getStringAttribute("item"));
String index = cacheStr(nodeToHandle.getStringAttribute("index"));
String open = cacheStr(nodeToHandle.getStringAttribute("open"));
String close = cacheStr(nodeToHandle.getStringAttribute("close"));
String separator = cacheStr(nodeToHandle.getStringAttribute("separator"));
ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, nullable, index, item, open, close, separator);
targetContents.add(forEachSqlNode);
}
}
private class IfHandler implements NodeHandler {
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
String test = cacheStr(nodeToHandle.getStringAttribute("test"));
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
}
}
自定义ExtMybatisXMLLanguageDriver继承MybatisXMLLanguageDriver
public class ExtMybatisXMLLanguageDriver extends MybatisXMLLanguageDriver {
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
MybatisXMLScriptBuilder builder = new MybatisXMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
}
修改项目的DefaultScriptingLanguage配置,修改为我们自己重写的.
/**
* @author nieqiurong
*/
@Configuration
public class MybatisPlusConfig {
@Bean
public ConfigurationCustomizer configurationCustomizer(){
return new ConfigurationCustomizer() {
@Override
public void customize(org.apache.ibatis.session.Configuration configuration) {
configuration.setDefaultScriptingLanguage(ExtMybatisXMLLanguageDriver.class);
}
};
}
}
实际项目测试结果
jdk1.8.291 mybatis-plus 3.3.2 237个mapper子类 大约减少了43%的堆内存占用
| 修改前 | 修改后 |
|---|---|
| 70502788 b = 67.23669815063477 Mb | 39937110 b = 38.08699607849121 Mb |
如果目前用的Mybatis-Plus版本比较低,但内存占用问题又困扰着你,可以试试此方法,这方法不只适用于Mybatis-plus,在Mybatis项目里也是能使用的.
Mybatis官方改动的话,其实只要改动两行代码也行,也能省不少内存,但会占用常量池,不过这种解决方案目前来说可能不是最优解.
// parseDynamicTags方法中的
String text = child.getStringBody("").intern();
// IfHandler
String test = nodeToHandle.getStringAttribute("test").intern();
另外还可以通过升级Jdk版本至9+,这样能白嫖Jdk的一些String优化.
结论
具体能减少多少堆内存视项目情况而定,项目里的表一些公共字段或者重复字段较多的话,这样减少效果会比较明显,如果整个项目都没什么重复字段,那就只能减少部分Mybatis-Plus一些特定的字符串,自然就不会太明显了.
更多技术讨论请添加我的微信: nieqiurong