祖传偏方,解决Mybatis-Plus堆内存占用过多

3,073 阅读5分钟

祖传诊断工具: VisualVm

分析原因

这里用的一个小型项目(49张表),用代码生成工具生成了代码,注入默认SQL方法,也就是 49 * 16 = 784 个 MappedStatement

2.png

1.png

3.png

通过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内容

4.png

下面我们再看两张图 (注意看两个String的内存地址)

6.png

7.png

我们从上面的图片可以看出,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 Mb39937110 b = 38.08699607849121 Mb

9.png

10.png

8.png

如果目前用的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