问题描述:

一天我正在专心致志的搬着砖,突然测试同事找我说服务器好像崩了,现在所有业务服务都调用不了直接就报错啦,赶紧看看啥问题。

服务器崩了这还了得,赶紧先等上管理中心瞄一眼,发现原本部署好的服务状态全部变为异常了。登上服务器后台用jmap查了下JVM各个代的内存使用率,发现老年代内存使用率已经到99.9%了,用jstat看也是发现一直JVM一直在做Full GC。毫无疑问这是堆内存溢出了。


原因分析:

问了下之前都有什么操作,测试也只是说就把服务都启动完之后调了几个接口就这样了。因为看现象是内存溢出的问题,为了快速复现,所以就把应用给重启了,但是给JVM分配内存调小了一半,并且部署的服务也减少到一半,然后再给JVM加上

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/dump

用来在JVM内存溢出时自动在指定的目录生成堆转储快照,然后交给同事用慢慢等着复现了。

过了一阵子,果然发现又内存溢出了,拿着新鲜出炉的堆转储快照文件分析看看究竟是什么原因。用MAT打开堆转储文件之后,等它解析完之后打开的第一个页面是:
在这里插入图片描述
这个界面会显示不同对象的内存占比,感觉没什么好看的,点击Leak Suspects发现最占内存比的对象是org.apache.ibatis.session.Configuration的实例,已经占了1.8G内存了。

点击左上方的Open Dominator Tree for entire heap,可查看每个对象占用内存大小,如下图:
在这里插入图片描述
从图中可见除了最上面两个,最占内存大小的就是下面连续的Configuration。

这里先用OQL语言查找出所有的Configuration实例,显示有75个,然后任一层一层的点下去看下里面都是什么占用了内存,可以看到主要是Configuration实例的两个属性:sqlFragments以及mappedStatements。

它们的类型都是StricMap,不熟悉的话大家可以去翻下mybatis中Configuration实现的源码,它有个内部类就是StrictMap,继承自HashMap是一个映射字典来着。

在Configuration中的sqlFragments作用主要是用来缓存mapper文件中sql节点,key为namespace+sql节点的id,value为sql节点内容转化成的XNode对象;而mappedStatements作用则是用来缓存mapper文件中的statement,即各种select、insert、update、delete节点。key也是namespace+各个节点的id,value为节点内容转化成的mappedStatement。

随意打开了几个Configuration实例查看里面sqlFragments和mappedStatements都缓存了哪些mapper的内容,居然发现这几个Configuration实例里面缓存的mapper文件大概都差不多,不信邪的再多点几个实例观察里面的mapper,随意瞄了几眼仍然能看到重复的。

也就是说着75个Configuration实例几乎都是把所有mapper文件都缓存了一道。

这里先说下我们应用结构,由众多独立且功能不同微服务组成(当然也按照一定规则来可以互相调用)。每个微服务都会给它配置一个spring应用上下文来管理bean加载、维护bean直接依赖、负责bean什么周期等等。
然后需要对数据库进行操作的微服务当然也要给对应的spring上下文配置好数据源什么的。其中有个org.mybatis.spring.SqlSessionFactoryBean(用来构造SqlSessionFactory的)有个属性mapperLocations是用来配置臊面mapper文件位置的。

一般来说功能不同的微服务基本上用到的数据库表也有不同,所以对应用到的数据库表映射的mapper文件也不同。就好比服务A如果就是更新A商品信息,那么只查A商品对应的数据库表A,也只缓存表A对应的mapper文件而不用去管B。也就是说正常情况下具体微服务需要用到哪些mapper就指定哪些mapper文件加载就好了。

但是现在翻了大部分spring应用上下文的配置文件发现都是用 * 来把所有的mapper文件包括进去了。本来单个微服务只需要用到几个mapper文件,因为偷懒不想一个个mapper文件指定或者一个个mapper文件目录指定而采用 * 来选定全部的mapper文件(起码有上百个了)。
这样不但浪费了大量的内存,就连服务启动速度也给拖慢了。


解决方案:

其实说起来这种全加载的,在自己负责的服务里面照样有这个写法。讲道理是不应该有的,只是以前自己刚上手的时候看别人怎么做自己也跟着怎么做了,并没有考虑其中是否有什么不妥。至于为什么一开始别人写的时候为什么会采用*来全加载而不是按需加载呢?这个就不得而知了。只是以后写代码看来还是要多多考虑一下,不能全用ctrl c+ctrl v呀。

最后有一个疑惑,感觉即使改成按需加载也只是在一定程度上缓解了内存缓存mapper的消耗。随着后面服务的不断增加,在缓存mapper上的内存消耗也是会越来越大的,而且有些服务要用到的数据库表肯定是有重复的,要缓存的mapper也会重复。
那么是否可以把spring应用上下文配置的SqlSessionFactoryBean抽出来公用呢?只需要选定所有mapper文件加载一次,后面其他服务都可以用这个SqlSessionFactoryBean而不用每个服务都需要维持自己的一份?

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐