为什么不建议3G以下的情况下使用CMS GC

之前曾经有讲过在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的话还得获取硬件信息等,挺折腾,另外毕竟还是想做到在能支撑的情况下尽可能不要丢弃这些日志信息),有此类场景经验来给点建议吧。

转自:https://www.iteye.com/blog/gao-xianglong-2179252

 

### CMS GC 中触发 Full GC 的原因及机制 #### 原因分析 在CMS(Concurrent Mark-Sweep)垃圾收集器的工作过程中,当老年代的空间足以容纳新晋升的对象时,可能会触发Full GC。具体来说: - 当执行CMS GC的过程中,如果存在对象需要从年轻代晋升至老年代,而此时老年代空间足,则会引发Full GC操作[^1]。 - 如果在CMS GC运行期间,由于浮动垃圾的存在导致临时性的空间足,也可能触发Full GC来清理这些垃圾并释放足够的空间。 另外,在某些情况下,即使Survivor Space有足够的容量用于存储存活下来的对象,但如果某个对象的大小超过了To Space的最大可用内存,那么此对象会被直接转移到老年代;一旦老年代无法提供相应的连续内存块给这样的大对象,也会造成Full GC的发生[(5)](https://docs.oracle.com/javase/8/docs/api/java/lang/System.html#gc--) [^2]. #### 触发机制详解 以下是几种可能导致CMS环境下发生Full GC的具体场景及其背后的触发机制: 1. **分配担保失败**: 在Minor GC完成后尝试将部分幸存者迁移到老年代之前,JVM会对目标区域的老年份剩余空间做预估计算。假如预测结果显示没有足够多的自由空间可供安置即将迁移过来的新成员们的话,就会启动一次完整的全局扫描过程——也就是所谓的Full GC动作来进行必要的调整优化工作直至满足需求为止[^3]. 2. **元数据溢出(Metaspace Out Of Memory)**: 尽管自JDK 7u40之后引入了Metaspace概念替代原有的PermGen设计模式从而减少了永久代满载的风险程度;然而仍然有可能因为加载类数量过多或者单个类定义过大等原因最终耗尽整个堆外专属分区资源进而迫使系统发起一轮全面清扫行动以腾挪更多宝贵位置供后续使用. 3. **显式调用System.gc()方法**: Java程序开发者有时出于调试目的或者其他特殊考量主动发出请求让虚拟机尽快安排一场大规模整理活动通过这种方式可以间接促使原本计划中的增量型维护转变为即时生效的整体性重构方案尽管现代版本已经弱化甚至屏蔽掉这种人为干预手段的影响但仍需留意其潜在后果. 4. **并发标记阶段未能及时完成而导致串行处理介入**: 正常状况下,CMS算法采用分步交错方式进行各项子任务之间相互配合协作实现高效运作效果可是偶尔会出现个别极端案例使得预定时间内没能顺利完成预期进度于是切换回传统方式重新开始新一轮更为彻底深入细致入微级别的排查整治流程这就是为什么我们常常能够观察到伴随着常规周期之外额外增加出来的几次短暂停顿现象实际上正是上述转换所引起的结果表现形式之一而已. ```java // 示例代码展示如何手动触发改行为 (推荐生产环境这么做!) public class ForceFullGCDemo { public static void main(String[] args) throws InterruptedException{ Runtime.getRuntime().exec("cmd /c start"); // 创建大量进程模拟压力测试 Thread.sleep(1000); System.out.println("Before calling System.gc()"); System.gc(); // 显式建议 JVM 执行 Full GC Thread.sleep(5000); System.out.println("After calling System.gc()"); } } ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值