物理内存耗尽、CMS GC碎片造成RT慢的两个Case

最近碰到的两个Case:一个是一个应用每天晚上集群里都有几台java进程退出的case;另一个是一个应用运行一段时间后在某种情况下RT突然变慢的Case,来看看排查过程。

Case I
第一个Case是每晚都有几台java进程退出,上机器看,dmesg | grep kill后发现是由于oom,所以os层面把进程给杀掉了,看了下当时的内存使用状况,发现物理内存快被用光了,说明这是一个堆外内存被用光的case,根据这个应用的特征,知道这个应用会用到大量的Direct Memory,看了下应用的启动参数,是没有配置MaxDirectMemorySize的,因此MaxDirectMemorySize的大小即等于-Xmx,从启动参数来看,-Xmx + MaxDirectMemorySize确实超过了物理内存的大小。

为什么会出现Direct Memory造成物理内存被耗光,就得说下Direct Memory的回收机制,Direct Memory是受GC控制的,例如ByteBuffer bb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆外占用1k的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。

为什么会有部分机器出现退出,而有些机器正常,按照上面的推测,主要就取决于有没有触发GC,从而回收堆外的Direct Memory,查看了正常的机器的gc log,确实触发了cms gc,而出问题的这些机器,则没有执行,这样就可以解释通了。

按照这样的分析,解决方法也就自然产生了,就是加上-XX:MaxDirectMemorySize,加上这个大小限制后,那么只要Direct Memory使用到达了这个大小,就会强制触发GC,这个大小如果设置的不够用,那么在日志中会看到java.lang.OutOfMemoryError: Direct buffer memory。

对于使用Direct Memory较多的场景,需要注意下MaxDirectMemorySize的设置,避免-Xmx + Direct Memory超出物理内存大小的现象。

Case II
有一个应用出现了非常奇怪的现象,就是运行了一段时间后会突然的出现rt从之前的50ms变成150ms左右的现象,但这个应用依赖的所有的后端的rt没什么变化,也就是应用的本身,但只要重启下应用就会恢复。

重启后就会恢复,因此决定用btrace一步一步跟,看看是哪里变慢了,但业务逻辑太复杂了,btrace不是很好跟,因此折腾了很久还是没看出太多的端倪。

业务方自己发现有个现象,就是在rt变慢的时候,gc的时间也会慢很多,在rt快的时候,ygc大概只需要15ms,而在rt变慢后,ygc大概需要45ms。

在采用cms gc的情况下,ygc变慢的原因通常是由于old gen出现了大量的碎片,因此猜测是这个问题,于是用jmap -histo:live强制执行了下full gc,执行完毕后,rt果然又从150ms降到了50ms。

因为是cms gc碎片问题造成的rt变慢,要解决还真的比较棘手,暂时只能是折腾成每天凌晨定时强制执行下full gc,悲催的碎片问题(所以还是那句话,如果不是因为heap size大,以及对rt比较敏感的话,能不用cms gc还是别用为妙)…

之前也碰过很多次cms gc的碎片问题,但通常是其造成的full gc导致的影响,而像这次case中这种对rt产生如此大影响的还真是第一次碰到(而且还有个诡异的现象,就是其实那些rt慢的机器在之前也有由于concurrent mode failure执行过full gc,但rt却没下降)。

—————————————————-
微信公众账号后台开放了之前青龙老贼透露的数据统计的功能,例如订阅公众账号的用户的增长状况、性别比例、省份比例,每篇文章的浏览、打开、原文链接和转发次数等,挺帅的。

我的这个账号的一些有趣的数字:
订阅者中MM的比例:5.81% (果然低呀)
订阅者中杭州用户的比例:31.92%(这样看来订阅者中阿里的不少)
文章的打开率: 50%左右… (真心不怎么高)

=============================
题图来源于:http://img.ifeng.com/tres/auto/9/2010/0531/107_4915254228_20100531165840.jpg
欢迎关注微信公众号:hellojavacases

关于此微信号:
分享Java问题排查的Case、Java业界的动态和新技术、Java的一些小知识点Test,以及和大家一起讨论一些Java问题或场景,这里只有Java细节的分享,没有大道理、大架构和大框架。

公众号上发布的消息都存放在http://hellojava.info上。

答:CMS GC会不会回收Direct ByteBuffer的内存

微信上有同学(也是同事)发消息问我这个问题,回答是:Oracle JDK 6u32前的版本不会。

Direct ByteBuffer是在Java Heap外分配内存,NIO等东西里使用的比较多,但Direct ByteBuffer分配出去的内存其实也是由GC负责回收的,而不像之前一篇文章里的Unsafe是完全自行管理的,Hotspot在GC时会扫描Direct ByteBuffer对象是否有引用,如没有则同时也会回收其占用的堆外内存,但不幸的是在6u32前的版本里,CMS GC有bug会导致可能回收不掉,具体的bug id为7112034,在链接的Backport信息里,可以看到这个bug是在hotspot 20.7的版本里修复的(hotspot的版本号通过java -version的最后一行Java Hotspot Version之类的可以看到),6u32带的就是这个版本,所以6u32是会回收的。

回收不掉的情况下会造成的问题是明明已经不用了,但堆外内存仍然被消耗掉,悲惨的情况下可能会导致堆外内存耗光。

Direct ByteBuffer除了上面这个bug可能造成堆外内存耗光外,还有一种场景也可能会造成堆外内存耗光,如Direct ByteBuffer对象晋升到了Old区,那这个时候就只能等Full GC触发(CMS GC的情况下等CMS GC),因此在Direct ByteBuffer使用较多,存活时间较长的情况下,有可能会导致堆外内存耗光(因为Direct ByteBuffer本身对象所占用的空间是很小的)。

对于上面这种类型的应用,最好是在启动参数上增加-XX:MaxDirectMemorySize=x[m|g],例如-XX:MaxDirectMemorySize=500m

这个参数默认的大小是-Xmx的值(在没设置MaxDirectMemorySize参数的情况下,用jinfo -flag等方式会看到默认值是-1,但VM.maxDirectMemory这个方法里发现是-1,则会以-Xmx作为默认值),此参数的含义是当Direct ByteBuffer分配的堆外内存到达指定大小后,即触发Full GC(这段逻辑请见Bits.reserveMemory的代码),如Full GC后仍然分配不出Direct ByteBuffer需要的空间,则会报OOM错误:
java.lang.OutOfMemoryError: Direct buffer memory

因为上面所说的状况,如碰到堆外内存占用较多的场景,可以尝试强制执行Full GC(强制的方法为执行jmap -histo:live)看看,多执行一两次,如堆外内存下降的话,很有可能就是Direct ByteBuffer造成的,对于这种情况,通常加上上面的启动参数就可解决。

很多情况下,我们会看到Java进程占用的内存超过-Xmx的大小,原因就是类似Direct ByteBuffer、Unsafe、GC、编译、自己写的JNI模块等这些是需要占用堆外空间的。

=============================
欢迎关注微信公众号:hellojavacases
关于此微信号:
用于分享Java的一些小知识点,Java问题排查的Cases,Java业界的动态,以及和大家一起互动讨论一些Java的场景和问题。

公众号上发布的消息都存放在http://hellojava.info上。