JVM 垃圾回收器总结
CMS回收器原理
浏览了目前网上关于CMS的一些文章,对CMS有解释的比较详细的但涉及到的知识点包含的很全面的并不多,本文汇总下CMS中的那些知识点
前言:
- 全称是 Concurrent Mark Sweep,采用标记清除算法
- CMS收集器对处理器资源非常敏感。占用了一部分线程资源而导致应用程序变慢,降低吞吐量(用户线程执行时间/(用户线程执行时间+垃圾回收时间))。CMS默认启动的回收线程数是(处理器核心数量 +3)/4,处理器核心数量越多,垃圾回收线程占比越小。但是当处理器核心数量不足四个时, CMS对用户程序的影响就可能变得很大。
- CMS回收器是针对老年代的回收器
- 目标是减小STW(stop the world:所有用户工作线程停止,只有垃圾回收线程运行)的时间,STW在任何垃圾回收器中都不可避免
- -XX:+UseConcMarkSweepGC开启CMS
- 可达性分析算法:GC Roots应用链上的对象都是正在使用的,其他无法通过直接或间接引用指向GC Roots的对象都是要被回收的垃圾对象
GC Roots包括:- 虚拟机栈上的对象
- 本地方法栈上的对象
- 方法区中静态变量引用的对象
- 方法区中常量引用的对象
- CMS只会回收老年代,不会收集年轻代;年轻代只能配合Parallel New或Serial回收器
- 空间分配担保机制:Young GC时,检查老年代连续可用的最大空间是否大于新生代的所有对象总空间,如果大于就行这次的GC,如果不大于会判断是否开启处理晋升失败开关HandlePromotionFailure
开启这个开关后会再判断老年代连续可用的最大空间是否大于历代晋升到老年代的对象的平均大小,如果大于就进行这次的GC,如果不大于或者未开启HandlePromotionFailure开关的话就进行Full GC
CMS的过程
1. 初始标记
这个过程是STW的,但由于这个过程只是标记GC Roots直接引用的对象,其他间接引用的不做处理,因此这个过程很快,STW的时间比较短
2. 并发标记
-
对之前标记出的GC Roots直接引用对象继续追踪标记引用链上的其他对象,此过程与用户线程并发进行
-
三色标记:对没有扫描过的对象置为白色,对已经扫描过自己但是后面引用链上的其他对象还没有全部扫描则置为灰色,一个对象包括后面引用链上的所有对象都扫描结束的话置为黑色。我们的扫描标记过程是从GC Roots,如果全部扫描结束,应该只有黑白两种颜色,白色就是从GC Roots不可达的对象,要被回收
-
用户线程并发进行,应用关系变更怎么办,标记就不准确了,只有满足以下两个条件才会出现漏标对象的情况
- 已经扫描过的黑色对象断开GC Roots的引用链
- 未扫描的白色对象加入到GC Roots的引用链
-
写屏障:这里的屏障可以类比于spring中的aop,在变更引用关系时拦截到这一操作,将变更的对象加入到C++实现的数据结构中。断开引用链时记录对象叫原始快照,加入引用链时记录对象叫增量更新,CMS采用的就是增量更新。可以看到CMS不关注引用断开这种情况,这样的垃圾对象只能在下次GC时回收,这种垃圾叫浮动垃圾。
-
卡表:跨代引用假说认为大多数对象是同代引用,很少出现跨代引用,不应该浪费太多资源处理这种情况,对老年代的堆内存划分成大小相等的卡片区域,一个卡片区域可能存在多个对象,通过写屏障记录下存在指向年轻代对象的卡片区域,将这一区域标记为 dirty,后面年轻代扫描时只对dirty的卡片进行扫描
-
Concurrent Mode Failure: 由于用户线程并发进行,当占用内存过大时就会出现Concurrent Mode Failure异常,此时可以搭配Serial Old回收器继续回收
3. 预清理
该过程依旧与用户线程并发,这个过程会对dirty的卡片再次扫描,年轻代和老年代都会再次扫描目的就是为了处理引用关系变更的问题
4. 可中断的预清理
这个阶段做的事情和预清理阶段类似,但是它存在的目的是尽量减少下一阶段重新标记时STW时间。当eden区使用达到特定的值时才会执行这一阶段,可以用CMSScheduleRemarkEdenSizeThreshold参数设置,默认是2M这一阶段循环执行,退出的条件可以通过参数设置循环次数,执行时间,Eden区的使用率等(具体参数为CMSMaxAbortablePrecleanLoops,CMSMaxAbortablePrecleanTime,CMSScheduleRemarkEdenPenetration)这一阶段很期望能够进行一次Young GC,可以减轻后面重新扫描年轻代的负担,可以通过参数CMSScavengeBeforeRemark强制触发一次Young GC,但如果Jvm 刚刚执行过一次Young GC,这里再强制执行就不合理了
5. 最终标记
这个过程是STW的,对年轻代和老年代都会重新进行最终的引用关系扫描
6. 并发清除
与用户线程并发,回收无效对象,释放内存
7. 标记重置
重置内部状态,准备进入下一个回收周期,与用户线程并发
触发时机
CMS有两种模式foreground和background,foreground是单线程STW的没有预清理的阶段且不压缩,触发条件一般是分配对象时空间不足
background会有CMS线程去循环判断触发条件默认2秒,触发条件有很多种情况:
- System.gc();
- UseCMSInitiatingOccupancyOnly没有开启时,JVM会根据统计数据动态判断CMS GC时间,第一次无统计数据时判断老年代内存使用占比达到50%进行CMS GC
- 参数UseCMSInitiatingOccupancyOnly开启老年代使用占比触发CMS,用
CMSInitiatingOccupancyFraction来设置具体的比例,默认是-1,源码中会取92% - 空间担保判断需要进行GC
- 元空间扩容,开启CMSClassUnloadingEnabled 前提下
缺点
- 无法处理浮动垃圾
- 使用标记清除算法产生空间碎片
- 新生代对象晋升失败和Concurrent Mode Failure时会执行Full GC进行碎片压缩,可以调低CMSInitiatingOccupancyFraction,尽早GC,但太小会频繁GC
- 执行System.gc()时会进行碎片压缩
- 参数 UseCMSCompactAtFullCollection(默认true)和CMSFullGCsBeforeCompaction设置多少次Full GC触发一次压缩,默认值为0,代表每次进入Full GC都会触发压缩
G1垃圾回收器原理
前言
G1回收器和CMS回收器的关注点都是提升JVM响应速度,G1回收器更适合应用在服务后端,未来规划中将替换掉CMS
Region
- G1回收器不再将堆空间划分成物理连续年轻代老年代,而是将空间划分成大小相同的Region区域,可以通过参数-XX:G1HeapRegionSize设定它的大小,范围是1到32M。某一个Region区域可能eden区,也可能是suvivor区或老年代,当某一对象的大小超过Region的一半时它将被存放到几个连续Region组成的Humongous区
- 每个Region区中有RSet(remember Set)和卡表,RSet中存放键值对,key是其他Region区的地址,表示这个Region区中有对象引用自己的Region
区中的对象,value是一个集合,存放的是key指向的Region区中引用自己的对象所在的卡表。Collection Set(CSet)记录了GC要收集的Region集合 - 记录对象引用关系的操作就是通过写屏障方式实现,这里的具体实现是原始快照SATB(Snapshot-At-The-Beginning)
- 并发标记阶段Region区域中会预留一部分空间给新的对象使用,有两个TAMS(top-at-mark-start)指针用来标识出哪些对象是新的,这些对象相当于被标记过不会回收
Young GC
G1回收器提供了Young GC模式和Mixed GC模式。当年轻代空间不足时触发Young GC,Young GC的过程和其他回收器类似,STW下执行对象从Eden区到suvivor的复制,可以有多个回收线程并行执行,对于老年代指向新生代的跨代引用不需要扫描全部的老年代,只需要从RSet中扫描引用自己Region区域的老年代Region即可
Mixed GC
- 这一过程会收集年轻代和部分老年代
- 触发时机: -XX:InitiatingHeapOccupancyPercen可以设置老年代占整堆比,默认45%,当老年代使用达到这一值时并不会立即进行Mixed GC,而是会等待一次Young GC
- 执行过程
- 初始标记: 标记GC Roots直接引用的对象 STW,就是Young GC中的过程
- 根分区扫描: Young GC完成所有对象从eden到survivor的迁移,后面就会将suivivor中的对象全部扫描并标记成根,suivivor分区也被称为根分区,扫描的是Survivor区中的老年代对象引用
- 并发标记: 扫描GC Roots引用链上的对象进行可达性分析,重新处理SATB记录下的在并发时有引用变动的对象。同时,并发标记过程中,会计算区域中存活对象的比例,后面的筛选回收阶段会用这些数据做价值最大化的计算
- 最终标记: 扫描并发阶段发生变化的对象,也要处理少量的SATB记录,空的区域被移除并回收,STW
- 筛选回收: STW,参数-XX:MaxGCPauseMillis指定一个G1收集过程目标停顿时间,默认值200ms,不过它不是硬性条件,只是期望值,根据停顿预测模型统计出的历史数据来预测本次收集的Region,从而尽量满足用户设定的目标停顿时间。
停顿预测模型的解释: G1收集器的停顿 预测模型是以衰减均值为理论基础来实现的,在垃圾收集过程中会记录每个 Region的回收耗时、脏卡数量等可测量步骤的花费成本,分析得出平均值、标准偏差、置信度等统计信息。这里强调的“衰减平均值”是指它会比普通的平均值更容易受到新数据的影响,平均值代表整体平均状态,但衰减平均值更准确地代表“最近的”平均状态。换句话说,Region的统计状态越新越能决定其回收的价值。 然后通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。 - 复制: 将存活的对象复制到未使用的region中,避免产生碎片
CMS和G1的比较
这个问题经常在大厂面试中问到,如果我们真的了解这两种回收器很容易回答这个问题
- 两个回收器的关注点都是获取最短停顿时间提高JVM的响应速度,像Parallel Scavenge的关注点则是达到一个可控制的吞吐量
- 数据存储实现上不同,CMS还是将堆内存划分成连续区域的年轻代老年代,但是G1则是物理上不连续的Region空间,降低了回收粒度,提升回收效率
- G1使用的是标记复制算法没有空间碎片,而CMS是标记清除算法会产生空间碎片
- G1中提供了年轻代的回收,CMS没有
- 由于底层存储的不同,两个回收器的实现过程上虽然整体看类似但具体实现上有很大区别,两个都有初始标记阶段,并发标记,重新标记和回收阶段,G1中不存在预处理阶段,筛选回收阶段有停顿预测模型计算要回收哪些Region以满足用户期望的回收时间;G1中使用写屏障实现原始快照用于处理并发标记阶段用户线程并发产生的引用变更,CMS是增量更新;G1中的记录集记录了指向自己的Region和该Region中的卡表集合,而CMS只有卡表变脏的处理
- 触发时机上,CMS的background模式会有线程去循环判断触发条件默认2秒,一般的判断条件是老年代使用占比值,上G1的Mixed模式是等待老年代占整个堆内存的比值且等待一次Young GC
其他回收器
Serial
历史悠久,针对年轻代回收,标记复制算法,单线程全程STW,JVM客户端模式下依然可用
ParNew
Serial的多线程版本,他们的实现代码上很多都是复用的,CPU如果是单核心的,采用多线程回收只会增加线程上下文切换的成本,除了Serial收集器外,目前只有它能与CMS 收集器配合工作。
Parallel Scavenge
和ParNew很像但关注点是是达到一个可控制的吞吐量(用户线程运行时间/(用户线程运行时间+垃圾回收时间)),架构中本身有PS MarkSweep收集器来进行老年代收集,并非 直接调用Serial Old收集器,但是这个PS MarkSweep收集器与Serial Old的实现几乎是一样的,所以在官 方的许多资料中都是直接以Serial Old代替PS MarkSweep
-XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间
-XX:GCTimeRatio参数 直接设置吞吐量大小, 例如设置19,也就是垃圾回收时间 : 用户线程运行时间是 19:1,
-XX:+UseAdaptiveSizePolicy 开启后不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区 的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数 了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间和最大的吞吐量
Serial Old
Serial的老年代版本,单线程收集器,使用标记-整理算法,可以作为CMS发生Concurrent Mode Failure的后备预案
Parallel Old
Parallel Scavenge收集器的老年代版本,标记-整理算法,Parallel Scavenge加Parallel Old是吞吐量优先的比较好的组合