物理内存耗尽、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上。

为什么不建议

之前曾经有讲过在heap size<=3G的情况下完全不要考虑CMS GC,在heap size>3G的情况下也优先选择ParallelOldGC,而不是CMS GC,只有在暂停时间无法接受的情况下才考虑CMS GC(不过当然,一般来说在heap size>8G后基本上都得选择CMS GC,否则那暂停时间是相当吓人的,除非是完全不在乎响应时间的应用),这其实也是官方的建议(每年JavaOne的GC Tuning基本都会这么讲)。

为什么给了一个这么“武断”的建议呢,不是我对CMS GC有什么不爽,相反CMS GC一直是我很热爱的一种GC实现,之所以建议在<=3G的情况下完全不要考虑CMS GC,主要出于以下几点考虑: 1、触发比率不好设置 在JDK 1.6的版本中CMS GC的触发比率默认为old使用到92%时,假设3G的heap size,那么意味着旧生代大概就在1.5G--2.5G左右的大小,假设是92%触发,那么意味着这个时候旧生代只剩120M--200M的大小,通常这点大小很有可能是会导致不够装下新生代晋生的对象,因此需要调整触发比率,但由于heap size比较小,这个时候到底设置为多少是挺难设置的,例如我看过heap size只有1.5G,old才800m的情况下,还使用CMS GC的,触发比率还是80%,这种情况下就悲催了,意味着旧生代只要使用到640m就触发CMS GC,只要应用里稍微把一些东西cache了就会造成频繁的CMS GC。 CMS GC是一个大部分时间不暂停应用的GC,就造成了需要给CMS GC留出一定的时间(因为大部分时间不暂停应用,这也意味着整个CMS GC过程的完成时间是会比ParallelOldGC时的一次Full GC长的),以便它在进行回收时内存别分配满了,而heap size本来就小的情况下,留多了嘛容易造成频繁的CMS GC,留少了嘛会造成CMS GC还在进行时内存就不够用了,而在不够用的情况下CMS GC会退化为采用Serial Full GC来完成回收动作,这个时候就慢的离谱了。 2、抢占CPU CMS GC大部分时间和应用是并发的,所以会抢占应用的CPU,通常在CMS GC较频繁的情况下,可以很明显看到一个CPU会消耗的非常厉害。 3、YGC速度变慢 由于CMS GC的实现原理,导致对象从新生代晋升到旧生代时,寻找哪里能放下的这个步骤比ParallelOld GC是慢一些的,因此就导致了YGC速度会有一定程度的下降。 4、碎片问题带来的严重后果 CMS GC最麻烦的问题在于碎片问题,同样是由于实现原理造成的,CMS GC为了确保尽可能少的暂停应用,取消了在回收对象所占的内存空间后Compact的过程,因此就造成了在回收对象后整个old区会形成各种各样的不连续空间,自然也就产生了很多的碎片,碎片会造成什么后果呢,会造成例如明明旧生代还有4G的空余空间,而新生代就算全部是存活的1.5g对象,也还是会出现promotion failed的现象,而在出现这个现象的情况下CMS GC多数会采用Serial Full GC来解决问题。 碎片问题最麻烦的是你完全不知道它什么时候会出现,因此有可能会造成某天高峰期的时候应用突然来了个长暂停,于是就悲催了,对于很多采用了类似心跳来维持长连接或状态的分布式场景而言这都是灾难,这也是Azul的Zing JVM相比而言最大的优势(可实现不暂停的情况下完成Compact,解决碎片问题)。 目前对于这样的现象我们唯一的解决办法都是选择在低峰期主动触发Full GC(执行jmap -histo:live [pid])来避免碎片问题,但这显然是一个很龌蹉的办法(因为同样会对心跳或维持状态的分布式场景造成影响)。 5、CMS GC的”不稳定“性 如果关注过我在之前的blog记录的碰到的各种Java问题的文章(可在此查看),就会发现碰到过很多各种CMS GC的诡异问题,尽管里面碰到的大部分BUG目前均已在新版本的JVM修复,但谁也不知道是不是还有问题,毕竟CMS GC的实现是非常复杂的(因为要在尽可能降低应用暂停时间的情况下还保持对象引用的扫描不要出问题),而ParallelOldGC的实现相对是更简单很多的,因此稳定性相对高多了。
而且另外一个不太好的消息是JVM Team的精力都已转向G1GC和其他的一些方面,CMS GC的投入已经很少了(这也正常,毕竟G1GC确实是方向)。

在大内存的情况下,CMS GC绝对是不二的选择,而且Java在面对内存越来越大的情况下,必须采用这种大部分时候不暂停应用的方式,否则Java以后就非常悲催了,G1GC在CMS GC的基础上,有了很多的进步,尤其是会做部分的Compact,但仍然碎片问题还是存在的,哎…

Java现在在大内存的情况下还面临的另外两个大挑战:
1. 分析内存的堆栈太麻烦,例如如果在大内存的情况下出现OOM,那简直就是杯具,想想dump出一个几十G的文件,然后还要分析,这得多长的时间呀,真心希望JDK在这方面能有更好的工具…
2. 对象结构不够紧凑,导致在内存空间有很高要求的场景Java劣势明显,不过这也是新版本JDK会重点优化的地方。
至于在cpu cache miss等控制力度上不如C之类的语言,那是更没办法的,相比带来的开发效率提升,也只能认了,毕竟现在多数场景都是工程性质和大规模人员的场景,因此开发效率、可维护性会更重要很多。

推荐几篇相关的文章:
1. A Generational Mostly-concurrent GC(CMS GC的理论论文)
2. The Pauseless GC Algorithm(可以管窥下Zing是如何实现不暂停compact的)
3. Understanding CMS GC log

最近在纠结一个问题,求有想法或建议的回下消息。
在一个打某种日志的场景中,如何做到避免打日志导致应用受影响,首先异步等是肯定的,但由于日志量巨大,所以仅仅异步还是会造成很大的IO压力,但限流的话到底怎么限比较合理呢?(例如根据IOPS?但IOPS的话还得获取硬件信息等,挺折腾,另外毕竟还是想做到在能支撑的情况下尽可能不要丢弃这些日志信息),有此类场景经验来给点建议吧。

=============================
欢迎关注微信公众号:hellojavacases

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

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