背景介绍
- es版本:6.3.2
- es集群配置:16核cpu,内存64G,磁盘200G
- JDK版本:1.8
- 垃圾回收器: CMS+ParNew
部署在这个集群的服务偶尔会遇到服务超时的情况,从kibana监控中可以看到,服务超时情况发生时,es服务器cpu较高。es存在young gc频繁,old gc 低频率,每天约出现2-4次。
查看过去一小时的监控情况,发现young gc 比较频繁,大量对象最终进入了老年代,通过old gc被回收掉了。
查看GC日志,log里99%都是GC (Allocation Failure)造成的young gc。Allocation Failure表示向young generation(eden)给新对象申请空间,但是young generation(eden)剩余的合适空间不够所需的大小导致的minor gc。
Desired survivor size 56688640 bytes, new threshold 6 (max 6)
- age 1: 6717288 bytes, 6717288 total
- age 2: 6025032 bytes, 12742320 total
- age 3: 987872 bytes, 13730192 total
- age 4: 176 bytes, 13730368 total
- age 5: 336 bytes, 13730704 total
- age 6: 93864 bytes, 13824568 total
复制代码
- Desired survivor size表示survivor区域允许容纳的最大空间大小为56688640 bytes
- max 6 表示对象经过6次gc后依然存活直接进入老年代
- 对象列表为此次gc之后,survivor当前存活对象的年龄大小分布,下次gc如果对象没释放的话,超过阈值的(age=6 or 占用空间 > 56688640)对象将晋升到old generation。
JVM 垃圾回收
当代主流虚拟机(Hotspot VM)的垃圾回收都采用“分代回收”的算法。“分代回收”是基于这样一个事实:对象的生命周期不同,所以针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率。
- 新生代:分三个区:一个Eden区,两个Survivor区,默认内存占比8:1:1。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到两个Survivor区(中的一个)。当这个Survivor区满时,此区的存活且不满足“晋升”条件的对象将被复制到另外一个Survivor区。对象每经历一次Minor GC,年龄加1,达到“晋升年龄阈值”后,被放到老年代。
- 老年代:在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代。
- java8已经没有持久代了,改为元数据区,主要存放元数据,例如Class、Method的元信息。
对象分配过程
- 对象比较大的时候,超过-XX:PretenureSizeThreshold设置值时,直接分配到老年代;
- 向eden申请空间创建新对象,eden没有合适的空间,因此触发minor gc
- minor gc将eden区及from survivor区域的存活对象进行处理:
- 如果这些对象年龄达到阈值(MaxTenuringThreshold),则直接晋升到年老代
- 若要拷贝的对象太大,那么不会拷贝到to survivor,而是直接进入年老代
- 若to survivor区域空间不够/或者复制过程中出现不够,则发生survivor溢出,直接进入年老代
- 其他的,若to survivor区域空间够,则存活对象拷贝到to survivor区域
- 此时eden区及from survivor区域的剩余对象为垃圾对象,直接抹掉回收,释放的空间成为新的可分配的空间
- minor gc之后,若eden空间足够,则新对象在eden分配空间;若eden空间仍然不够,则新对象直接在年老代分配空间
垃圾回收器
- 新生代收集器有:Serial(单线程),ParNew(多线程),Paraller Scavenge(侧重于吞吐量控制)
- 老年代收集器有:CMS(获取最短回收停顿时间为目标的回收器,该回收器是基于“标记-清除”算法实现的), Serial old,Parallel Old
- G1收集器可作用与新生代和老年代(JDK9默认垃圾收集器)
ParNew+CMS工作机制
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75 //老年代内存使用率超过75%触发垃圾回收
-XX:+UseCMSInitiatingOccupancyOnly
复制代码
ParNew:复制算法,将内存分为大小相等的两块,每次使用其中的一块一块用完时,将存活的对象复制到另一块。 CMS:使用标记-清除算法。整个过程分为四步:
- 初始标记:STW,标记GC Roots能关联到的对象,速度很快
- 并发标记:GC Roots Tracing过程。耗时。和用户线程一起执行(并行)
- 重新标记:STW,标记并发标记过程中程序运行导致标记变化的对象,时间比初始标记长,远比并发标记短
- 并发清除:耗时。和用户线程一起执行(并行)
G1
G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:
- G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
- G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。并基于用户指定的停顿时间来选择进行垃圾回收的区块数量。G1 采用增量回收的方式,每次回收一些区块,而不是整堆回收。
- G1 收集线程在标记阶段和应用程序线程并发执行,标记结束后,G1 也就知道哪些区块基本上是垃圾,存活对象极少,G1 会先从这些区块下手,因为从这些区块能很快释放得到很大的可用空间,这也是为什么 G1 被取名为 Garbage-First 的原因。
G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址。如下图所示:
Remembered Sets(Rset)
逻辑上说每个Region都有一个RSet,RSet记录了其他Region中的对象引用本Region中对象的关系。
Collection Set(CSet)
记录了GC要收集的Region集合,集合里的Region可以是任意年代的。
G1工作模式
- YoungGC年轻代收集
在分配一般对象(非巨型对象)时,当所有eden region使用达到最大阀值并且无法申请足够内存时,会触发一次YoungGC。每次younggc会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。
- mixed gc
当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。
问题解决
通过指标数据,发现es集群存在新生代内存分配过小,导致young gc 频繁。 通过命令查看
jstat -gc pid 1000 1000
发现新生代内存仅分配了约1g,而老年代占到了29g。一种解决方案就是增大新生代,具体大小需要根据经验和调整后指标数据决定。
此外,调研发现,一些大的互联网公司,如美团,携程,es的垃圾回收器使用的都是G1。综上,直接替换es的垃圾回收器为G1。
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly
复制代码
直接替换为
-XX:+UseG1GC
-XX:MaxGCPauseMillis=50
复制代码
效果对比图
未升级的机器:
升级为G1的机器:
- 升级前young gc平均耗时约为15ms,升级后约为1ms
- 升级前young gc比较频繁,升级后young gc 次数明显减少
- 升级前cpu偶尔有毛刺现象,升级后cpu整体比较稳定
对ES使用G1以后,Young GC的频率和耗时都可以极大的降低,Old GC几乎不会出现。
作者:Sophie May
链接:https://juejin.cn/post/6934892512444317732