CMS GC 6点优化方向小结

979 阅读11分钟

常见的优化方向总体来说分为3个预防措施3个解决措施_CMS GC优化.png

基本的知识体系构建

堆空间的划分

image.png 在jdk8中移除了jdk7中永久代的概念,取而代之的是MetaSpace这个区域,在jdk7中将字符串常量池、静态常量池以及Class元信息全部存储在了永久代中,在jdk8中进行优化,将字符串常量池、类静态变量、符号引用等几项移动到Heap中,将类的元信息移动到一个堆外内存区域中,有效解决了jdk7中jvm发生OOM的问题。

重要的概念点

GC是什么意思? GC指代三种概念,不能提到GC就只联想到其中的某一个问题

  1. GC垃圾收集的技术
  2. GC垃圾收集器
  3. GC垃圾收集的过程 Mutator 生产垃圾的程序,垃圾的制造者,通过收集器Allocator进行allocate垃圾和free垃圾 分代假说概念
  • 弱分代假说:绝大多数的对象声明周期都很短,绝大多数的对象都是朝生夕灭的

jvm young区域中eden:to:from区域的比例为8:1:1,这个值是与弱分代假说相关的

  • 强分代假说:熬过越多次垃圾收集过程的对象越难以消亡

TLAB

Thread Local Allocation Buffer的简写,基于CAS的独享线程,可以优先将对象分配在Eden区域中的一块内存,因为Java线程独享的内存区没有锁竞争,所以分配速度十分快,每个TLAB都是线程独享的。

Card Table

中文翻译为卡表,主要用来标记卡页面的状态,每个卡表项对应一个卡页,当卡页中一个对象引用有操作的时候,写屏障会标记对象所在的卡表状态为dirty,卡表的本质是用来解决跨代引用的问题。

垃圾收集算法

分为两种标记算法和三种垃圾清理算法,两种标记算法分别为标记引用计数法、GC Roots Tracing,三种垃圾清理算法分别为标记-复制算法、标记-清除算法和标记-压缩算法,根据GC器的不同使用不同的算法。

对象分配内存

  • 空闲链表:通过额外存储的记录空闲的地址,将随机IO变为顺序IO,但是带来了额外的性能消耗
  • 碰撞指针:通过一个指针作为分界点,需要分配内存时,仅需要把指针向空闲的一段移动与对象大小相等的距离,分配速率较高,但是使用场景有限。

CMS垃圾收集器

CMS收集器是一种获取最短回收停顿时间为目标的收集器,目前很大一部分的Java应用集中在互联网网站的服务端上,这类应用通常会较为关注服务器的响应速度,希望系统停顿时间尽可能短,给用户带来良好的交互体验,CMS收集器非常符合这类应用的需求。 整个收集过程分为四个阶段:

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除 其中初始标记、重新标记这两个阶段仍然需要STW,初始标记仅仅是标记一下GC Roots能直接关联到的对象,速度很快;并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程。重新标记阶段修正因为并发标记阶段用户程序继续运作导致标记产生变动的那一部分对象的标记记录,这个停顿的时间通常会比初始标记阶段更长一点,但也远比并发标记阶段的时间短;最后的并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时工作的。

GC优化目标

Mutator

Mutator的类型根据对象存活时间比例图来看主要分为两种,在弱分代假说中也提到了类似的说法,如下图所示,"Survival Time"表示对象存活的时间,"Rate"表示对象分配比例

  • IO交互型:互联网上目前大部分的服务都属于该类型,例如分布式RPC、MQ、HTTP网关服务等,对内存的要求不太大,大部分对象在TP9999的时间内都会死亡,Young区域越大越好
  • MEM计算型:主要是分布式计算Hadoop,分布式存储,Old区域越大越好 评价GC的两个核心指标
  • 延时:也可以理解为最大停顿时间,即在垃圾收集过程中一次STW的最长时间,越短越好,一定程度上可以接受频次的增大,GC技术的主要发展方向
  • 吞吐量:应用系统生命周期内,由于GC线程会占用Mutator当前刻中的CPU时钟周期,吞吐量为Mutator有效花费时间站系统总运行时间的百分比,吞吐量优先的收集器可以接受就较长时间的停顿。一次停顿的时间不超过应用服务的TP9999,GC的吞吐量不小于99.99%。 当然,除了二者之外还有介于两者之间的情景,本片文章主要讨论第一种情况,对象Surivial Time分布图,对于我们设置GC参数有非常重要的指导意义。 fimlet

CMS基本参数的设置

//首先必须要打开CMS收集器,jdk8中默认收集器并不是CMS
-XX:+UserConcMarkSweepGC

//打开参数的显示
-XX:+PrintGCDetails
-XX:+PringtCommandLineFlags
-XX:+PrintHeapAtGC

//输出GC日志,使用https://gceasy.io/网站可以简单快速地分析GC的情况
-Xloggc:D://gceasy//waterGC.log

需要预防的问题:动态扩容、System.GC()、过早晋升问题

1.关闭扩容,固定堆空间(预防)

服务刚刚启动的时候GC次数会比较多,最大剩余空间很多,但是依然会有很多 的GC发生,这种情况可以通过观察GC日志或者通过监控工具来观察堆内的空间变化情况即可,GC Cause一般为Allocation Failure,且在GC日志中会观察到一次GC的发生后,堆内的参数大小会被调整

//固定heap空间
-Xms4096m
-Xmx4096m
//固定young区域的大小    
-XX:NewSize=
-XX:MaxNewSize=
//固定元空间MetaSpace的大小
-XX:MetaSpaceSize=
-XX:MaxMetaSpaceSize=

一般来说,需要保证Java虚拟机的堆是稳定的,确保**-Xms和-Xmx设置的是一个值,获得一个稳定的堆空间,在不追求停顿时间的情况下震荡的空间也是有利的**,可以动态地伸缩以节省空间,舍友表示菜鸟一年要给阿里云交几千万的云服务器费用,利弊如何取舍需要考虑实际的情况。

2.System.GC()的去留(预防)

CMS的Old区域的收集行为有两种模式,分别是BackGroung GC和ForeGround GC,BackGround GC是常规意义下的CMS收集行为,ForeGround GC则会触发一次整堆的GC,System.GC()在被调用的时候会触发一次ForeGround GC

为什么System.GC()会触发一次ForeGround GC 在jvm的内存区域中又有一块内存区域叫做Direct Byte Buffer,有着零拷贝的特点,在Netty等框架中广泛地被使用,这个区域的垃圾无法通过常规的GC器进行收集,而是需要sum.misc.Cleaner这个类来对DBF进行垃圾收集,Cleaner在收集的过程中会显式的调用System.GC(),消除掉整堆中的垃圾,防止堆中某一块内存区域对DBF存在引用,而导致DBF中内存区域无法进行回收

//降低System.GC()的触发等级
-XX:+ExplictGCInvokesConcurrent
//关闭类卸载开关
-XX:+ExplictGCInvokesConcurrentAndUnloadClass

3.过早晋升问题

Full GC发生的次数比较频繁,每次Full GC后Old区域的变化比例非常大

  • Young区域空间设置的过小
  • 分配速率过大 那么如上文所述,如果jvm实例对应的是一个RoocketMQ,一个SpringCloud GateWay组件,那么新生代的填充速度绝对是非常迅速的,新生代填充的速度比较快,这样会导致jvm去动态的调整CMS的MaxTenuringThresHold参数,这个参数的默认值是6,假如新生代的对象分配速率过大,jvm会计算对象年累的累加值,如果这个累加值在i=个值的时候大于了1/2,那么JVM的MaxTenuringThresHold就会调整为i值,这样只会使得更多对象更早的进入老年代,进入老年代之后又很快失去了自己的引用,就要触发GC来对老年代进行垃圾回收,GC并不能解决掉对象分配速率过快的问题,因此只能不断地进行垃圾回收,导致老年代的GC图样会变成锯齿状 flag 这个问题看似比较简单,但是这一个参数的调整可以带来十分大的收益,如果jvm实例对应的应用是一个交互型的应用,那么新生代分配的空间就应该比老年代更大
-Xmn2560m

需要解决的问题

MetaSpace OOM

上文中已经提到,MetaSpace主要存放一些类的元信息,这些数据的生命周期和类加载器的生命周期是相同的,导致这种问题的关键原因就是ClassLoader不停地在内存中load新的Class,一般这种问题都发生在动态类加载的问题上,经常出现问题的点还是集中在反射、JavaSisit字节码增强、CGLIB动态代理等技术点上,另外就是需要即时给MetaSpace区域的使用率增加一个监控,如果指标发生波动就需要提前发现并且解决问题

CMS GC发生的过于频繁

Old区域频繁地发生CMS GC每次的耗时并不是特别场,整体最大STW也在可接受的范围当中,但是由于GC太过于频繁导致吞吐下降地比较快 这种情况比较常见,基本都是一次YoungGC完成后,负责处理CMS GC的一个后台线程concurrentMarkSweepThread会不断轮询,使用shouldConcurrentCollect()方法做一次检测,判断受否需要进行Old GC的发生

  • 如果开启了-XX:UseCMSInitiatingOccupancyOnly参数,判断当前Old区域的使用率是否大于了阈值,如果大于了阈值则触发CMS GC,如果没有设置默认值为92%
  • 如果之前的Young GC失败过,或者下次Young区执行Young GC可能发生失败,这两种情况都需要触发CMS GC 这种问题之下一般是发生了内存的泄露问题,频繁的GC仍然不能够将内存的使用空间降低到一个较低的水平,这种情况下需要进行内存泄露的分析。
  • 内存Dump,在CMS GC的前后发生各进行一次Dump
  • 分析Top Component,按照对象、类、类加载器等多个维度观察直方图,同时使用outgoing和incoming分析关联的对象
  • unreachable对象

单词CMS Old GC耗时过长

CMS GC单词STW最长超时1000ms,不会频繁地发生,如下图所示最长达到了8000ms,某些场景下会引起“雪崩效应”,这种场景十分危险,应该尽量避免出现这种情况 longtimeGC 重点关注初始标记最终标记的两个阶段

问题一般主要出在最终标记上面

Final Remark 的开始阶段与 Init Mark 处理的流程相同,但是后续多了 Card Table 遍历、Reference 实例的清理并将其加入到 Reference 维护的 pend_list 中,如果要收集元数据信息,还要清理 SystemDictionary、CodeCache、SymbolTable、StringTable 等组件中不再使用的资源

对于FinalReference的分析主要观察java.lang.ref.Finalizer对象的dominator tree,找到泄露的来源,经常会出问题的几个点又Socket的SocksSocketImpl、Jersey的ClientRuntime、Mysql的ConnectionImpl等等。另外,jdk8会开启 CMSClassUnloadingEnabled,这样会使得CMS-Remark阶段尝试进行类的卸载,打开-XX:-CMSClassUnloadingEnabled来避免MetaSpace的类卸卸载处理

总结

un jvm优化四步走

  1. 深入理解java虚拟机这本书不得不看,掌握GC基本的知识体系,基本的JVM分析工具
  2. 了解基本的GC的评价方法,摸清如何设定独立系统的指标,以及在业务场景中判断GC是否存在问题的手段
  3. 进行实际的场景优化
  4. 总结优化经验,不断重复这个过程