01. 垃圾回收算法
垃圾回收算法-种类
- 标记-清除(mark-sweep)
- 标记-压缩(mark-compact)
- 复制算法(copy)
- 分代收集算法(Generational GC)
标记-清除(mark-sweep)
- 原理:
1.标记死亡的对象为空闲内存,并记录在一个空闲链表(free-list)之中。
2.清除确定不可用的对象。
- 缺点:
1. 内存碎片化: 堆的对象必须是连续分布的,总空间内存足够,无法分配
2.分配效率较低:对于空闲链表,需要逐个访问列表中的项,来查找能够放入新建对象的空闲内存。
标记-压缩(mark-compact)
- 原理:把存活的对象聚集到内存区域的起始位置,从而留下一段连续的内存空间。
- 目的:解决内存碎片化问题
- 缺点:压缩算法的性能开销大,效率低
复制算法(copy)
- 原理:把内存区域等分,分别用两个指针from和to来维护,并且只用from指针指向的
内存区域来分配内存,发生垃圾回收时,把存活的对象复制到to指针指向的区域
并且交换from指针和to指针的内容。
- 目的:解决内存碎片化的问题,效率较高
- 缺点:堆空间的使用的效率极其低下
分代收集算法(Generational GC)
- 原理:对象的生命周期不同,针对对象的生命周期不同,采用不同的垃圾回收算法,提高回收效率。
- 分代:新生代:存放新创建(new)的对象,对象的生命周期很短,每次新生代垃圾回收(Minor GC)
只有少量对象存活,选用复制算法效率较高,少量的复制成本可以完成回收。
老年代:存放新生代经历N次垃圾回收后,仍然存活晋升的对象。该区域对象存活率高
老年代的垃圾回收(Major GC), 使用Mark-Sweep-Compact(标记-清除-压缩)
新生代内又分三个区:一个Eden区,两个Survivor区(一般而言),大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到两个Survivor区(中的一个)。当这个Survivor区满时,此区的存活且不满足“晋升”条件的对象将被复制到另外一个Survivor区。对象每经历一次Minor GC,年龄加1,达到“晋升年龄阈值”后,被放到老年代,这个过程也称为“晋升”。显然,“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间,在Serial和ParNew GC两种回收器中,“晋升年龄阈值”通过参数MaxTenuringThreshold设定,默认值为15。
02. 垃圾回收器
垃圾回收器-常用组合
- Serial + Serial Old (+XX:+UseSerialGC)
- Parallel Scavenge + Parallel Old (-XX:+UseParallelGC -XX:+UseParallelOldGC)
- ParNew + CMS ( +XX:+UseParNewGC +XX:+UseConcMarkSweepGC)
- G1 (-XX:+UseG1GC):
并行和并发的区别
这两个名词都是并发编程中的概念,在谈论垃圾收集器的上下文语境中,可以这么理解这两个名词:
1、并行Parallel
多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
2、并发Concurrent
指用户线程与垃圾收集线程同时执行(但并不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上
Serial + Serial Old (+XX:+UseSerialGC): GC 线程在做事情时, 其他所有的用户线程都必须停止 (即 STW, stop the world);
Serial + CMS: 一般不会这样配合使用;
ParNew + CMS (+XX:+UseConcMarkSweepGC): 新生代的 GC 使用 ParNew, 有多个 GC 线程同时进行 Young GC (主要是在多核的环境用多线程效果会好); 而老生代使用 CMS;
ParNew + Serial Old (+XX:+UseParNewGC): 新生代用 ParNew 的时候, 也可以选择老生代不用 CMS, 而用 Serial Old, 这个组合也不太常用;
Parallel Scavenge + Serial Old (+XX:+UseParallelGC): Parallel Scavenge 收集器的目的是达到一个可控制的吞吐率 (适用于各种计算任务); 这个组合中老生代仍旧使用 Serial Old;
Parallel Scavenge + Parallel Old (+XX:+UseParallelOldGC): 新生代使用 Parallel Scavenge, 而 Parallel Old 是老年代版本的 Parallel Scavenge;
G1 (-XX:+UseG1GC)::新生代和老年代都使用 G1 垃圾回收器。
https://www.jianshu.com/p/2a1b2f17d3e4 — 简书
03. 老年代垃圾收集器CMS
老年代垃圾收集器CMS-概念
- 什么是CMS
- CMS全称 Concurrent Mark Sweep并发的,使用标记-清除算法的垃圾回收器。
- 老年代使用CMS垃圾回收器,需要添加参数:“-XX:+UseConcMarkSweepGC”
- 牺牲吞吐量为代价,来获得最短回收停顿时间的垃圾回收器。
吞吐量的理解:
吞吐量专注于在特定时间段内最大限度地提高应用程序的工作量。吞吐量如何测量的例子包括:
在特定时间内完成的交易数量。
批处理程序可以在一个小时内完成的作业数量。
可以在一个小时内完成的数据库查询的数量。
对于侧重于吞吐量的应用,高暂停时间是可以接受的。由于高吞吐量应用程序在较长时间内集中在基准测试上,所以快速响应时间不是一个考虑因素。
老年代垃圾收集器CMS-设计目标
垃圾收集时,避免出现长时间卡顿:
手段
- 不对老年代进行整理,死亡对象所占用的内存标记为空闲内存,使用空闲列表(free-list)管理内存空间。
- mark and sweep(标记清除)阶段大部分工作和用户线程一起并发执行。
老年代垃圾收集器CMS-优势和劣势
优势
- 并发收集,低停顿
劣势
- 内存碎片化:服务长时间运行,造成严重的内存碎片化。
- 对CPU资源敏感:需要多核CPU来支持CMS线程和应用线程之间的并发执行,多数时候都有部分CPU资源被GC消耗,CPU资源受限的情况下,CMS会比并行GC吞吐量差一些。
老年代垃圾收集器CMS-适用场景
适用场景
- GC过程短暂停,适合对延迟要求较高的服务,用户线程不允许长时间停顿的服务。
- 服务器是多核CPU,并且调优目标是降低延迟。
说明:如果服务器是多核CPU,并且主要调优目标是降低延迟, 那么使用CMS是个很明智的选择. 减少每一次GC停顿的时间,会直接影响到终端用户对系统的体验, 用户会认为系统非常灵敏。 因为多数时候都有部分CPU资源被GC消耗, 所以在CPU资源受限的情况下,CMS会比并行GC的吞吐量差一些。
CMS原理-周期性old GC
- 后台线程ConcurrentMarkSweepThread 线程循环判断(默认2s),判断是否满足触发条件来触发CMS GC
说明:周期性Old GC,执行的逻辑也叫Background Collect,对老年代进行回收,在GC日志中比较常见,由后台线程ConcurrentMarkSweepThread循环判断(默认2s)是否需要触发。
CMS原理-周期性old GC-触发条件
触发CMS-GC
- 老年代的使用率达到阈值CMSInitiatingOccupancyFraction,默认是92%
- 永久代的使用率达到阈值CMSInitiatingPermOccupancyFraction,默认92%
- 新生代的晋升担保失败:
1. 老年代剩余的空间不够容纳目前新生代的对象(已经使用的空间eden+from space)
2. 老年代剩余空间不够容纳历史平均每次ygc晋升到old的内存大小
CMS线程是否会做CMS gc 判断的
1.标准不仅仅是根据CMSInitiatingOccupancyFraction和UseCMSInitiatingOccupancyOnly参数来确定的,
2.比如新生代的to space不为空或者老年代剩余的空间不够容纳之前根据数据统计的平均每次ygc晋升到old的内存大小;
3.或者老年代的剩余空间不够容纳目前新生代已经使用的空间(eden + from space)
思考: Full GC的发生时间?
首先,什么时候可能会触发STW的Full GC呢?
Perm空间不足;
CMS GC时出现promotion failed和concurrent mode failure(concurrent mode failure发生的原因一般是CMS正在进行,但是由于老年代空间不足,需要尽快回收老年代里面的不再被使用的对象,这时停止所有的线程,同时终止CMS,直接进行Serial Old GC);
统计得到的Young GC晋升到老年代的平均大小大于老年代的剩余空间;
主动触发Full GC(执行jmap -histo:live [pid])来避免碎片问题。
http://hllvm.group.iteye.com/group/topic/41018
https://km.sankuai.com/page/35432218 —悲观策略
CMS原理-周期性old GC-过程
日志为CMS GC 的过程:
条件满足的时候,采用“标记清理”算法对老年代进行回收,过程可以说是很简单,标记存活的对象,清理掉垃圾对象;
但是为了实现整个过程的低延迟,实际上算法远远没有那么简单,过程整个过程分为如下几部分:
- 阶段1:Initial Mark(初始标记).
单线程执行(CMS线程),整个过程 STW
1. 标记GC Roots可达的老年代对象
2. 遍历新生代对象,标记可达的老年代对象
- 阶段2:Concurrent Mark(并发标记).
GC线程和应用线程并发执行
- 遍历InitialMarking标记出来的存活的对象,继续递归标记这些对象可达的对象。
- 可能发生新生代的对象晋升到老年代、或者直接在老年代分配对象,或者更新老年代对象的引用关系等等。这就是为什么需要后续的重新标记阶段?
- 阶段3:Concurrent Preclean(并发预清理).
GC线程和应用线程并发执行,可以通过CMSPrecleaningEnabled 关闭
- 处理并发阶段在新生代Eden区中分配了一个A对象,A对象引用老年代的B对象(B对象之前没有被标记),这个阶段会标记B为活跃对象。
- 并发标记阶段,老年代中有对象内部引用发生变化,会通过card mark把引用发生变化的对象标记为Dirty Card(脏区)。
- 这些被标记出来的Dirty Card(类似Card Table的数据结构 ModUnionTalble)在预清理阶段,通过扫描这些table,重新标记那些在并发标记阶段引用被更新的对象。
(晋升到老年代的对象,原本就在老年代的对象)
- 这些Dirty Card, 在这个阶段完成后,大部分已经被处理过了(清理了)
- 阶段4:Concurrent Abortable Preclean(并发可取消的预清理)
- 发生前提:新生代Eden区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold 默认是2M
- 存在的价值:尽最大的努力去处理并发阶段被应用线程更新的老年代对象,为STW 的Final remark清理障碍,Final remark阶段可以少处理一些,暂停时间也会相应降低。
- 主要循环做的两件事:
1. 处理新生代From和To区的对象,标记可达的老年代对象
2. 和上一个阶段一样,扫描处理Dirty Card中的对象
- 中断循环的条件:(持续的时间)
1. 执行上面逻辑的时间达到了阈值 CMSMaxAbortablePrecleanTime,默认5s
2.循环的次数大于CMSMaxAbortablePrecleanLoops设置的次数,默认是0
3.新生代Eden区的内存使用率达到了阈值 CMSScheduleRemarkEdenPenetration 默认50%
此阶段说明:
如果在循环退出之前,发生了一次YGC,对于后面的Remark阶段来说,大大减轻了扫描年轻代的负担,但是发生YGC并非人为控制,所以只能祈祷这5s内可以来一次YGC。
CMSMaxAbortablePrecleanTime来设定可中断预清理阶段的执行周期 置最多循环的次数 CMSMaxAbortablePrecleanLoops,默认是0
CMSScheduleRemarkEdenPenetration 设置启动重新标记阶段时Eden区的空间占用率
发生前提:如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段。
存在的价值:因为CMS GC的终极目标是降低垃圾回收时的暂停时间
https://blogs.oracle.com/jonthecollector/did-you-know — 此阶段循环的时间(显著影响STW停顿的持续时间)
- 阶段5:Final Remark(最终标记) - STW(时间长)
目标:完成老年代中所有存活的对象的标记。
作用范围:扫描整个堆对象来判断对象是否存活,需要扫描新生代(对象的数量)。
主要干的事情:重新标记并发阶段没有处理完或者新产生的引用关系
1. 遍历新生代对象,重新标记 CMSScavengeBeforeRemark - 强制YGC
2. 根据GCRoots,重新标记
3. 遍历老年代的Dirty Card,重新标记
- 可能产生的新引用关系
1. 老年代的新对象被GC Roots引用
2. 新生代对象指向老年代的对象被删除
3. 老年代未标记的对象被新生代引用
灰色对象已经不可达,但仍然需要扫描的原因:新生代GC和老年代的GC是各自分开独立进行的,只有Minor GC时才会使用根搜索算法,标记新生代对象是否可达,也就是说虽然一些对象已经不可达,但在Minor GC发生前不会被标记为不可达,CMS也无法辨认哪些对象存活,只能全堆扫描(新生代+老年代)。由此可见堆中对象的数目影响了Remark阶段耗时。分析GC日志可以得出同样的规律,Remark耗时>500ms时,新生代使用率都在75%以上。这样降低Remark阶段耗时问题转换成如何减少新生代对象数量。
此阶段说明:
在第一步骤中,需要遍历新生代的全部对象,如果新生代的使用率很高,需要遍历处理的对象也很多,这对于这个阶段的总耗时来说,是个灾难(因为可能大量的对象是暂时存活的,而且这些对象也可能引用大量的老年代对象,造成很多应该回收的老年代对象而没有被回收,遍历递归的次数也增加不少),如果在AbortablePreclean阶段中能够恰好的发生一次YGC,这样就可以避免扫描无效的对象。
CMS算法中提供了一个参数:CMSScavengeBeforeRemark,默认并没有开启,如果开启该参数,在执行该阶段之前,会强制触发一次YGC,可以减少新生代对象的遍历时间,回收的也更彻底一点。
不过,这种参数有利有弊,利是降低了Remark阶段的停顿时间,弊的是在新生代对象很少的情况下也多了一次YGC,最可怜的是在AbortablePreclean阶段已经发生了一次YGC,然后在该阶段又傻傻的触发一次。
- 阶段6:Concurrent Sweep(并发清除)
- 主要干的事:移除不用的对象,回收垃圾对象占用的内存空间,并且为将来使用
- 缺点:由于是并发执行的,会产生新的垃圾(浮动垃圾),新垃圾在此次GC无法清除
- 阶段7:Concurrent Reset(并发重置)
- 主要干的事:重新设置CMS算法内部的数据结构,准备下一个CMS生命周期的使用
After CMS
补充-CardTable概念
- 问题:Minor GC 为何不用对整个堆进行垃圾回收?
- 目的:卡表是为了减少老年代的全堆的空间扫描。
- 原理:卡表是将老年代空间切分成512字节若干张卡,卡表本身是单字节数组,数组中的每一个元素对应一张卡,当发生老年代引用新生代时,虚拟机将该卡对应的卡表元素设置成为适当的值。例如卡表3被标记为脏卡。minor GC 时,通过扫描卡表可以快速识别哪些卡中存在老年代指向新生代的引用,通过空间换时间的方式,避免了全堆扫描。
总结来说,CMS的设计聚焦在获取最短的时延,为此它“不遗余力”地做了很多工作,包括尽量让应用程序和GC线程并发、增加可中断的并发预清理阶段、引入卡表等,虽然这些操作牺牲了一定吞吐量但获得了更短的回收停顿时间。
补充-GC-Roots概念
- 可以理解为由堆外指向堆内的引用,GC Roots包括(但不限于)如下几种:
1. java方法栈中的局部变量
2.已加载的类的静态变量
3.JNI handles
4.已经启动且未停止的java线程
CMS垃圾回收调优参数
- CMS-GC参数:
1.-XX:+UseConcMarkSweepGC:
2.-XX:+UseCMSInitiatingOccupancyOnly:使用手动定义开始CMS收集,禁止hotspot自行触发CMS GC
3.-XX:CMSInitiatingOccupancyFraction=60:老年代使用60%后开始执行CMS
4.-XX:CMSFullGCsBeforeCompaction = 0:运行多少次full GC后进行内存压缩
5.-XX:+UseCMSCompactAtFullCollection:full GC时候, 开启对年老代的压缩
6.-XX:+CMSScavengeBeforeRemark:CMS remark之前,强制进行一次YGC
- 线上CMS-GC配置
[JVM参数设置、分析]:
参考:
实战调优 https://tech.meituan.com/jvm_optimize.html
实战撸源码 https://km.sankuai.com/page/35432218 —
【CMS-GC的一些疑问】 http://hllvm.group.iteye.com/group/topic/41018
【abortable_preclean源码实现】https://www.zhihu.com/question/65946581
【understanding-cms-gc-logs】https://blogs.oracle.com/poonam/understanding-cms-gc-logs
【R大VM目录】http://rednaxelafx.iteye.com/blog/362738
【CMS悲观策略源码解读】https://km.sankuai.com/page/35432218
实践
【海量连接服务端jvm参数调优杂记】 : https://www.jianshu.com/p/051d566e110d
【一个有意思的CMS的问题】 https://www.jianshu.com/p/a322309b1d90
【美团GC优化案例】 https://tech.meituan.com/jvm_optimize.html
Face your past without regret. Handle your present with confidence.Prepare for future without fear. keep the faith and drop the fear. 面对过去无怨无悔,把握现在充满信心,备战未来无所畏惧。保持信念,克服恐惧!一点一滴的积累,一点一滴的沉淀,学技术需要不断的积淀!