某某某系统堆内存使用率连续3次超过设定阈值98%。

问题背景:

13号晚上,系统突然发了一条上述短信,说是堆内存超过阈值了,我们之前出现过这样的问题,当时我们将堆内存扩大了一倍,在运行一段时间之后,又出现了这样的问题,说明并不是堆内存不够用,而是系统存在堆内存泄漏问题。次日,我们排查了一下这个问题,先将我个人的排查过程记录下来。

排查过程:

首先我们查看了一下监控图表,上面显示的信息是:在很短的一段时间内,系统进行了多次full GC,每次full Gc都是在系统到达阈值,并且报警之后才去执行,但是奇怪的是young GC 也还算正常,并不是特别频繁,那么问题就可以转化为,为什么有很多对象Gc年龄都超过了15进入到了年老代?或者是因为有很多大对象直接进入了年老代(我们的jvm配置中并没有配置直接到年老代的对象大小阈值,因此应该是超过伊甸园区大小的对象会直接到达老年代,我们的年轻代大小为1.3G,eden:survivor 默认8:1 因此对象达到了1.1G以上才会直接到老年代)?

那么我们就要找找为什么那么多对象在年轻代没有回收,一直混到了老年代,我们将堆内存日志在MAT中分析了一下,MAT中显示如下所示:
\"在这里插入图片描述\"
上图表示在堆内存中,byte数组占用堆大小达到了93M,也是占用最多的对象,然后再看了一下这个对象的引用,看是什么东西不让byte数组释放,如下图所示:
\"在这里插入图片描述\"
从上图我们可以看出引用这个数组对象的几个大的对象都是属于netty的,并且netty对消息的编解码也会耗费很多的堆内存,这个不能说是异常情况,为了更好的定位问题,我将现在线上正常运行机器的堆内存日志放到MAT中进行对比了一下,下图是正常机器的堆内存占用排行:
\"在这里插入图片描述\"
相差了30多M,说明这个占用特别不稳定,很有可能就是它的问题,这些都是netty底层引用的,并且是byte数组,因此首先想到的应该是bytebuf没有释放,因为bytebuf的底层实际就是byte数组,因此我在网上查了关于netty内存泄漏的问题,终于找到一些有用的信息:
\"在这里插入图片描述\"
上图说是netty在触发读事件执行的方法中,不会去主动释放消息缓存,需要手动释放,或者nettyhandler继承SimpleChannelInboundHandler,然后我看了一下这个类的源码,确实在read方法中执行了释放动作,如下图所示:
\"在这里插入图片描述\"
说实话,我当时很兴奋,因为我应该找到问题所在了,但是也很奇怪,为什么netty的官方示例以及一些权威书上的示例并没有这样做,更奇怪的是,没有执行释放的话也是运行很久之后才会出问题,而且有些机器并没有出问题。这个以后需要再深究一下。//TODO

到现在为止,我认为我已经找到问题的所在了,当我想要在线上试一下的时候,有个同事说,他在jvisualvm 上分析的堆内存日志结果并不是这样的,下图是在jvisualvm 上显示的堆内存占用结果:
异常机器堆内存结果:
\"在这里插入图片描述\"
正常机器堆内存结果
\"在这里插入图片描述\"
神奇的事情发生了!int[]对象居然占用了1G多!这个才是导致问题的根源吧! 回头想想,byte数组顶天也就一百来M…所以,在这里还是建议大家使用jdk提供的原生工具…同一个堆内存文件,mat把这个int[]给漏了?

int[]?不是byte[]? 那和ByteBuf就没有关系了?难道我之前的分析都错了?

我在mat中看了一下int[]对象的为什么会占用那么大(虽然mat可能分析的有问题,但是毕竟功能强大…),结果如下图所示:
\"在这里插入图片描述\"

\"在这里插入图片描述\"
我们可以看到,堆内存中存在大量的int[4096]数组(难怪占用这么大),并且引用源头依然指向netty,那问题来了,我不释放byteBuf应该是byte[]才是,那int[]是怎么来的?
后来我尝试着在源码里追踪了一下Int HashMap,也就是引用int[4096]的对象,但是后来我放弃了…太复杂了…

那现在该怎么办?

后来我突然想到了我之前看过说是netty有自己的垃圾回收机制(为了避免频繁的内存分配给系统带来负担以及GC对系统性能带来波动,netty4之后采用了自己的内存结构),netty把内存分为两大块,一个是堆外内存(direct内存),主要负责io的内存分配(这样可以节省从jvm到内核的内存拷贝),一个是堆内内存(heap内存),主要负责数据的编解码和业务处理等内存的分配。既然是编解码,那就免不了和各种类型数组产生关联,再回头看看我们之前的分析,其实占用堆内存大的前几条都是数组,也就是说,根本原因应该是编解码,那编解码完了之后程序会做什么?会触发读事件,也就是说会触发read()方法,那如果在这个方法里不去释放的话,netty自己不去回收这些数组,jvm更不会去回收,然后就造成了堆内存报警。

后来把netty改成手动释放缓存之后就正常了…

收藏 打印