内存溢出了!!!

某日规则配置人员告知系统报错,经排查发现系统内存溢出了:ERROR o.a.c.c.C.[.[.[.[dispatcherServlet].log 181 - Servlet.service() for servlet [dispatcherServlet] in context with path [/reu] threw exception [Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: GC overhead limit exceeded] with root cause

java.lang.OutOfMemoryError: GC overhead limit exceeded

这里先说一些我们这个系统的模块说明,系统的功能就是规则校验,规则底层使用的是drools,系统分2个模块,一个是管理模块A,一个是规则执行模块B,A 模块只部署了一台服务,B模块部署了9台服务,A模块主要是用来进行规则配置,B模块中发布了正式的规则,是订单需要规则校验的地方,然后A模块会与B模块进行交互,交互的场景就3种,1、规则配置好后进行规则校验2、规则配置好后进行规则测试 3、规则配置好后正式发布。3种场景都是由A模块调用B模块,具体的动作都是在B模块执行。然后规则校验、规则测试这2种场景都是由A模块调用B种指定的一台服务器(9台种的一台),规则正式发布才会对B的9台服务器全部发布。

现在内存溢出就在那台指定用于规则测试和规则校验的服务器,其余的8台均正常,内存使用比较稳定,没有出现内存一直缓慢增长的情况。但是用于校验和测试规则的那台服务器从监控页面上发现内存有一直缓慢增长的现象。所以怀疑应该是规则校验和规则测试这2个接口哪里由内存泄露。

带着猜想对指定的规则校验和规则测试2个接口于本地和测试环境进行压测,发现内存都能够正常回收,没有出现一个缓慢增长的现象。可能猜错了,只能找运维同事拿下当时的堆转储文件进行分析了。

打开MAT,加载堆转储文件,点击支配树:

可以看到第一个对象占用了30%的内存,占用了1.4个G,这里多一嘴,这个Retained Heap指标可以简单理解为只要回收了这个对象就能够回收的内存量,展开这个对象:

可以看出org.drools.compiler.kie.builder.impl.KieRepositoryImpl$KieModuleRepo$2中引用了几百个HashMap,每个HashMap都是M级别的,多的达到了70来M,所以关键点应该在org.drools.compiler.kie.builder.impl.KieRepositoryImpl$KieModuleRepo$2这个对象上,那看下这个对象是谁引用了(再多一嘴:with incoming references 是看有哪些对象引用了这个对象,with outgoing references 是看这个对象引用了哪些对象):

如上图红框部分为我们业务代码,那就可以从对应的业务代码上入手排查,但是我个人习惯反向排查问题,既然是oldKieModules这个对象中占用了大量的内存,那我们就到代码中找下对应类的这个属性:

从源码中可以看出oldKieModules就是一个LinkedHashMap,那么直接看下oldKieModules在哪里进行了put操作:

只有上面一个地方进行了put,直接在对应的方法上打上断点进行调试。

这一块代码的大致逻辑:这个是drools规则引擎中的一个规则模块,就是规则的相关资源都会加载到这个kieModule中,kieModules保存了当前的规则,然后oldKieModules保存的是旧的规则,从上面的代码可以看出oldKieModules进行put的场景是:加载一个已经存在kieModules中的相同版本规则时才会将旧规则保存到oldKieModules,如果不同版本的话是不会保存到oldKieModules中的。

但是我们系统的业务场景是不会进行加载相同的版本的规则的,如果规则有变动的话,我们的版本号是会升级的,也就是说正常oldKieModules中应该为空的才对。

不管那么多,先调试看看情况,第一次进入,oldKieModules是并没有进行put操作:

第二次进入:

调试发现发布规则的时候这个方法进入了2次,版本号是一样的,但是对应的调用方法源头不一样,第一次是createKJar方法触发的,第二次是deployJar方法触发的,而且这2处方法都是我们的业务代码。

查看下对应的业务代码:

deployJar(createKJar(releaseId, null, s.getValue()));

private KieModule deployJar(byte[] jar) {
    final Resource jarRes = kieServices.getResources().newByteArrayResource(jar);
    return kieServices.getRepository().addKieModule(jarRes);
}

private byte[] createKJar(ReleaseId releaseId, String pom, List<Resource> drls) {
    KieFileSystem kfs = kieServices.newKieFileSystem();
    if (pom != null) {
        kfs.write("pom.xml", pom);
    } else {
        kfs.generateAndWritePomXML(releaseId);
    }
    drls.forEach(drl -> {
        drl.setSourcePath("src/main/resources/" + drl.getSourcePath());

        kfs.write(drl);
    });
    final KieBuilder kb = kieServices.newKieBuilder(kfs).buildAll();
    if (kb.getResults().hasMessages(Message.Level.ERROR)) {
        throw new RuntimeException(kb.getResults().toString());
    }
    final InternalKieModule kieModule = (InternalKieModule) kieServices.getRepository().getKieModule(releaseId);
    return kieModule.getBytes();
}

从上面的代码可以看出先用createKJar方法进行一次规则构建(构建动作在kieServices.newKieBuilder(kfs).buildAll()这一步。这次构建会保存在kieModules中,但不会保存到oldKieModules中),然后构建完后使用kieServices.getRepository().getKieModule(releaseId)这一步从kieModules又将构建的资源拿出来,再放入到deplayJar方法中进行第二次构建构建,这一次构建出来的对象与第一次构建的对象不是同一个,然后新的对象会加入kieModules中,老的对象会加入到oldKieModules中,这样无形中内存扩大了一倍了。所以按理来说将deployJar(createKJar(releaseId, null, s.getValue())) 这行代码给调整成createKJar(releaseId, null, s.getValue()),相当于去掉了一次规则构建,也就能省下一半的内存了。

在测试环境进行验证一把:

未调整代码前(也就是每个规则会进行构建加载2次):

调整代码后(去掉了deployJar方法):

不管是总内存使用量还是对象上来看,效果都很显著,oldKieModules由原来的80多M直接变为0.06KB了(这0.06KB是oldKieModules本身对象占用的内存)。

再看一个有趣的点,未调整代码前的:

KieModules中的对象与oldKieModules中的对象不一样,一个100个对象,一个200个对象,按道理应该2个对象的个数一致才对,怎么kieModules才100个对象,刚开始看到这个数据吓一跳,以为哪里有bug,后来经过调试发现HashMap中put方法中有个afterNodeInsertion方法,afterNodeInsertion方法中会判断要不要移除最早的一个元素,而默认的removeEldestEntry方法是为false的,所以一般我们使用的时候是没有发现元素莫名被删除的情况,但是这里kieModules对象是重写了removeEldestEntry的,就是判断当前元素是否大于MAX_SIZE_GA_CACHE的值,而MAX_SIZE_GA_CACHE定义的就是100:

也就是说kieModules中最多只会有100个元素,再加的话就会删除最早的元素了,所以这就是为什看到的kieModules中的元素只有100个的原因。但是另一个问题又来了,如果删除最早的元素的话,那么我们的最早加载的规则不是会有问题吗?那怎么生产上一直没有出过错?我的猜测是虽然这个对象被移除kieModules这个map中了,但是创建的对象应该是被其他的对象引用了。在kieModules中随意找一个对象进行验证:

这次终于猜测对了,从with incoming references的结果中可以看到我们选中的对象有被4个对象引用,图中的①就是我们那个kieModules引用了,然后②③④中展开查找的话会发现源头都是我们那个业务代码中的kieContainerMap引用了(这里②和③未展开)。因为我们使用规则的时候是从keiContainerMap拿对应的对象进行跑规则的,所以在kieModules中没有相应的规则也没有关系。突然发现kieModules中将所有规则全移除貌似也没有问题,这个没有验证,但是意义也不大,因为即使将规则对象从kieModules中移除后,这个对象还在其他的地方引用了,并不会真正减少内存,这也正好印证了上面那个“Retained Heap这个指标是能够回收的内存量”,因为kieModules中的对象还被其他的对象引用了,而oldKieModules中的对象没有被其他的对象引用,所以kieModules的Retained Heap 比oldKieModules 的Retained Heap少很多:

到这其实也还是并没有发现内存泄露的点,因为即使调整了上面的业务代码(删除deployJar)也只是不会构建2次对象,也就是oldKieModules中不会有多余的对象。但是因为这次的堆中最大的就是oldKieModules这个对象,先暂时调整代码部署一版,将oldKieModules这一块的内存先释放出来,到时候看看还会不会内存溢出,如果再有内存溢出的话,下次排查的话应该就能够真正定位到原因了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值