某天凌晨,手机突然告警,线上某台机子内存使用率超过90%,当时以为是有定时任务在跑,再加上夜已深了,没有去排查具体原因。第二天早上有发布,内存降下来了,白天就没有去调查这个问题。等到傍晚高峰期的时候,又接到内存调用超过90%的告警,并且持续一段时间后,线上的某台机子挂了。
当时一台机子挂掉后,马上重启了挂掉的机子,并且把另一台机子的内存信息dump下来。
jmap -dump:format=b,file=文件名 [pid]
在dump过程中遇到小插曲,无法dump下来。
这种情况是因为非当前线程用户导致,在命令前面加上 sudo -u 用户
即可
同时保存了线程信息
sudo -u jetty jstack pid > /tmp/jstack2018-04-18.txt
查看服务器系统日志
cat /var/log/messages
上图看到 Killed process 7364, UID 502, (java) total-vm:10511252kB, anon-rss:7489308kB
java内存使用了7G多。ps:上一行的java进程的2627813 和 1872416都是页数,每页4K。
可以发现容器内存使用因为超过系统内存被kill掉了。
使用mat分析具体内存泄漏问题。
由于本人对mat工具使用的还不熟练,看到分布图后,total一共才103.6MB,以为堆内内存没有问题,便往堆外内存溢出方向去考虑。陆陆续续的检查了代码中的ThreadLocal的使用,对象也有及时清理,检查了线上线程的数量,都没有发现明显问题。
转载一篇线程过多导致的堆外内存溢出文章: 线程数过多导致堆外内存溢出
继续使用mat分析,使用Leak Suspects功能,发现内存中有大量的TrueTypeFont对象
正好想到在该应用中,有使用到字体画图,发送图片的功能。于是乎查询一下OOM当时请求情况,发现确实有大量的画图的请求,并且发现某个请求中同时画600多张画。
问题重现
接着,在公司的测试环境下模拟了下请求,果然内存使用率由60多一下子升到了80多。
回头检查了一下画图的代码,发现在处理字体的地方确实有问题。
该方法的逻辑为加载服务器上的某个字体文件,使用画笔画一张二维码,然后保存到服务器的临时目录下。
这里在加载字体的时候没做好处理,导致每次过来一个画的任务就会加载一次字体文件,内存中创建一个字体对象,而我们服务器上的字体文件大小约有16M,也就相当于每次画一幅画就需要加载16M的内存大小。
由于当时对mat不熟悉,还在怀疑是不是Font操作了堆外空间导致的。于是乎准备测试一下堆外空间。
排除堆外空间溢出可能性
我们服务器的配置为8G内存,jvm配置为 -Xmx4428m -Xms4428m -Xmn2767m,
堆外空间如果不限制的话,会和jvm使用差不多。这时候,我们限定堆外空间大小,比如我们指定堆外空间比如说只有100M -XX:MaxDirectMemorySize=100m
。
如果说Font使用的是堆外空间,那么堆外空间就会很快到达100M,并且进行Full GC,阻塞所有请求,Stop The World。这时候,只要Full GC清理及时,后面阻塞请求继续进来,继续满100M,继续Full GC。这样,内存使用率永远也不会到8G,就不会出现被系统kill掉的情况了。
但是在测试过程中,发现内存使用率还是在一路飙升,最终还是被kill了。 所以后来只能再次怀疑是堆内内存溢出。用同样的策略,将堆内内存设置为1G(100M应用起不来了),发现内存没有继续往上升,并且查看了下gc日志,一直在进行Fulll GC,从这大致可以看出使用的是堆内的内存。
由此可以看出,当时高峰期,大量画图请求进来,导致大量的大对象加载进内存,最终导致jvm内存超过了系统内存,容器被系统kill掉。
将Font改为单例模式,内存中只存在一份。
在公司测试环境再次试了下,内存使用率没有变化。发布上线后,线上该问题没有再复现,问题暂时告一段落。
后续调研发现,当请求量小的时候,内存上去后没有OOM,但是内存一直没有往下降,怀疑出现了内存泄漏。使用mat的Histogram搜索TrueTypeFont
再查看他的根回收节点。
过滤掉弱引用等对回收没有影响的引用。
发现主要有这么几个类,我们看最多的类Disposer。上网查了下Disposer的作用。
在mat中,我已经过滤掉了弱引用,剩下的发现有个FontStrikeDisposer对TrueTypeFont为强引用。但是这些数量比较少,只有8个,应该起不到作用才对。到此,头绪就断了,回头撸撸TrueTypeFont的源码看能不能有什么发现。