最新GC垃圾回收器发展史
垃圾回收器种类
串行:Serial/Serial Old
并行:ParNew/Parallel Scavenge/Parallel Old
并发:CMS/G1
新时代:ZGC/Shenandoah/Zing/Epsilon GC
垃圾回收器回收范围
经典垃圾收集器的使用组合方式
- 红色虚线是JDK8中废弃了的组合方式,但是并不是说就不能这么使用了(JDK9之后就完全不能按照红线的方式组合使用了)
- CMS和Serial Old的组合中,是由于CMS是并发的垃圾收集器,需要在垃圾满之前回收,否则会回收失败,当回收失败之后,需要Serial Old来顶上进行垃圾回收
- 绿色的虚线是JDK14中弃用了Parallel Scavenge和Serial Old的组合方式
- JDK14中移除了CMS
- JDK9之后G1变为默认的垃圾收集器
- JDK8中默认使用Prallel Scavenge和Parallel Old
- JDK15正式发布ZGC/Shenandoah
对象的分配
垃圾对象回收之前,需要了解一下对象是怎么分配内存空间的,对象分配一个堆地址空间有两种一种是直接分配和间接分配
- 直接分配 即多个工作线程同一时刻生成的对象一起竞争一个地址空间 cas 无锁竞争
- 在每个工作线程的TLAB中预分配,然后当该TLAB空间用完并分配新的TLAB时候才会同步锁定
什么时候才能进行STW
当到达一定的使用内存比例后然后分配对象时内存不足只有进入安全点或安全区域进行GC,并不是想暂停用户线程就可以随时GC
什么时候进行GC
- 各种Young GC的触发原因都是eden区满了
- Serial Old GC/PS MarkSweep GC/Parallel Old GC的触发则是在要执行Young GC时候预测其promote的object的总size超过老生代剩余size
- CMS GC的initial marking的触发条件是老生代使用比率超过某值
- G1 GC的initial marking的触发条件是Heap使用比率超过某值
- Full GC for CMS算法和Full GC for G1 GC算法的触发原因很明显,old算法不赶趟了,只能全局范围大搞一次GC
垃圾回收器算法
Serial收集器
- Serial采用复制算法、串行的垃圾回收方式(串行必然存在STW)
- 单线程收集器(垃圾收集时必须暂停其它所有工作线程)
- 新生代收集器
Serial Old收集器
- 使用“标记-整理”算法
- 单线程收集器
- Serial收集器的老年代版本
ParNew收集器
- 复制算法
- 多线程收集器
- 新生代收集器
Parallel Scavenge收集器
- 复制算法
- 多线程收集器
- 新生代收集器
- 吞吐量优先
- GC自适应的调节策略,通过其自身的监控,可以动态的监控内存的分配情况,自动的平衡吞吐量和低延迟或者有偏向的进行优化
Parallel Old收集器
- 标记整理算法
- 多线程收集器
- 老年代收集器
CMS CardTable
如何快速的找到所有可达对象呢?
-
GC Roots是垃圾收集器寻找可达对象的起点,通过这些起始引用,可以快速的遍历出存活对象。
-
现代JVM,堆空间通常被划分为新生代和老年代。由于新生代的垃圾收集通常很频繁,
如果老年代对象引用了新生代的对象,那么,需要跟踪从老年代到新生代的所有引用,从而避免每次YGC时扫描整个老年代,减少开销。
怎么避免老年代全部扫描 -
也就是说怎么解决跨代引用,避免扫描整个老年代
-
对于HotSpot JVM,使用了卡标记(Card Marking)技术来解决老年代到新生代的引用问题。具体是,使用卡表(Card Table)和写屏障(Write Barrier)来进行标记并加快对GC Roots的扫描。
-
怎么给cardtable打标记 就用到了write barrier机制,简单来说就是每次年老代对象中某个引用新生代的字段发生变化时(即old.a=new)
通过写屏障对该老年代所在cardtable区域进行打标
CMS收集器是一种以获取最短回收停顿时间为目标的收集器
- 标记清除算法 内存碎片化
- 并发多线程
- 第一次实现了让垃圾收集线程与用户线程可同时工作
- 老年代收集器
- CMS垃圾回收执行失败(CMS运行期间预留的内存无法满足程序需要,会出现Concurrent Mode Failure),这是JVM将要启动后备方案,临时启动Serial Old收集器重新进行老年代垃圾回收。
为什么CMS的GC线程可以和用户线程一起工作
CMS为了让GC线程和用户线程一起工作,回收的算法和过程比以前旧的收集器要复杂很多。原因就是因为GC标记对象的同时,用户线程还在修改对象的引用关系。因此CMS引入了三色算法,将对象标记为黑、灰、白三种颜色的对象,并通过「写屏障」技术将用户线程修改的引用关系记录下来,以便在「重新标记」阶段可以修正对象的引用。
回收过程
- 初始标记(CMS initial mark) STW
- 并发标记(CMS concurrent mark) (解决漏标 三色标记+增量更新)
- 重新标记(CMS remark) STW
- 并发清除(CMS concurrent sweep)
CMS的缺点
1、对处理器敏感
并发标记、并发清理阶段,虽然CMS不会触发STW,但是标记和清理需要GC线程介入处理,GC线程会占用一定的CPU资源,进而导致程序的性能下降,程序响应速度变慢。CPU核心数多的话还稍微好一点,CPU资源紧张的情况下,GC线程对程序的性能影响非常大。
2、浮动垃圾
并发清理阶段,由于用户线程仍在运行,在此期间用户线程制造的垃圾就被称为“浮动垃圾”,浮动垃圾本次GC无法清理,只能留到下次GC时再清理。
3、并发失败
由于浮动垃圾的存在,因此CMS必须预留一部分空间来装载这些新产生的垃圾。CMS不能像Serial Old收集器那样,等到Old区填满了再来清理。在JDK5时,CMS会在老年代使用了68%的空间时激活,预留了32%的空间来装载浮动垃圾,这是一个比较偏保守的配置。如果实际引用中,老年代增长的不是太快,可以通过-XX:CMSInitiatingOccupancyFraction参数适当调高这个值。到了JDK6,触发的阈值就被提升至92%,只预留了8%的空间来装载浮动垃圾。
如果CMS预留的内存无法容纳浮动垃圾,那么就会导致「并发失败」,这时JVM不得不触发预备方案,启用Serial Old收集器来回收Old区,这时停顿时间就变得更长了。
4、内存碎片
由于CMS采用的是「标记清除」算法,这就意味这清理完成后会在堆中产生大量的内存碎片。内存碎片过多会带来很多麻烦,其一就是很难为大对象分配内存。导致的后果就是:堆空间明明还有很多,但就是找不到一块连续的内存区域为大对象分配内存,而不得不触发一次Full GC,这样GC的停顿时间又会变得更长。
针对这种情况,CMS提供了一种备选方案,通过-XX:CMSFullGCsBeforeCompaction参数设置,当CMS由于内存碎片导致触发了N次Full GC后,下次进入Full GC前先整理内存碎片,不过这个参数在JDK9被弃用了。
G1收集器
特点
- 在延迟可控的情况下,获得尽可能高的吞吐量 保证低停顿高吞吐性能、
- 并行与并发:G1能充分利用多CPU、多核环境使用多个CPU或CPU核心来缩短Stop-The-World停顿时间
- G1将堆拆成一系列的分区(heap region),这样在一个时间段内,大部分垃圾回收操作就只是针对一部分分区执行,而不是整个堆或整个(老年)代,从而满足在指定的停顿时间内完成垃圾回收的动作。
- 空间整合:不会产生内存空间碎片,收集后可提供规整的可用内存,整理空闲空间更快
- G1将内存分为一个一个的region,内存回收是以region为基本单位的,每个heap区(Region)的大小在JVM启动时就确定了. JVM 通常生成 2048 个左右的heap区, 根据堆内存的总大小,区的size范围允许为 1Mb 到 32Mb;分多少个Region算法是判断是否是设置过堆分区大小,如果有则使用;没有,则根据初始内存和最大分配内存,获得平均值,并根据Region的个数得到分区的大小,和分区的下限比较,取两者的最大值。
- G1新生代的回收方式是并行回收,采用复制算法,所以不存在内存碎片化问题。但是整体上来看实际属于标记-压缩算法。通过复制算法处理之后的内存会被整齐的摆放在一起(即region经过垃圾回收之后会被规整),这两种算法都可以避免内存碎片化的产生。
- 可预测的停顿(它可以有计划的避免在整个JAVA堆中进行全区域的垃圾收集)
- G1跟踪各个region中的垃圾的堆积的“价值”大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表Csets,根据每次允许的收集时间,优先回收价值最大的region
- JAVA堆内存布局与其它收集器存在很大差别,它将整个JAVA堆划分为多个大小相等的独立区域(Region),虽然还保留了新生代和老年代的概念,但新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合
- G1收集器中,使用Remembered Set来避免全堆扫描
- 没有物理的分代,但由逻辑的分代
- G1是自适应扩展内存空间的,根据预测停顿时间来动态调整region大小
回收过程
G1的Young gc的具体回收过程需要STW,并且是多线程并行完成
G1的mix gc具体回收过程
混合回收分为两个阶段:并发标记和垃圾回收,其中并发标记阶段可以分为:初始标记子阶段,并发标记子阶段,再标记子阶段和清理子阶段。垃圾回收阶段一定发生在并发标记阶段之后。
第一阶段:并发标记
1)初始标记子阶段
2)并发标记子阶段( 解决漏标 SATB+三色标记)
3)再标记子阶段
4)清理子阶段
第二阶段:垃圾回收
-
初始标记子阶段 (需要STW)
它发生在YGC上。标记可能引用老年代对象的幸存者区域(根区域)
根区域扫描
扫描幸存者区域以获取对老年代的引用。这会在应用程序继续运行时发生。该阶段必须在年轻 GC 发生之前完成。 -
并发标记子阶段
当YGC执行结束之后,如果发现满足并发标记的条件(分配的内存超过内存总容量的45%),并发线程就开始进行并发标记。根据新生代的Survivor分区以及老生代的RSet开始并发标记。
-
再标记子阶段 (需要STW)
找出所有未被访问的存活对象(漏标的对象)。使用一种称为SATB更新 的算法,该算法比 CMS 收集器中使用的增量更新算法快得多。
空区域被移除并回收。现在计算所有区域的区域活跃度。
-
清理子阶段 (需要STW和并发)
- 统计存活对象。(STW)
- 重置RSet。(STW)
- 把空闲分区(全都是垃圾对象的分区)放到空闲分区列表中(并发)
- 额外信息处理 对整个堆分区中完全空白的老生代和大对象分区进行释放,对于其他的分区处理RSet,主要是分区的RSet粒度,如果发生了变化,那么变化前的数据结构可以被清除
该阶段清理操作并不会清理垃圾对象,也不会执行存活对象的拷贝 -
复制 (STW)
复制存活的对象到新的未使用的heap区(new unused regions).
只在年轻代发生时日志会记录为[GC pause (young)]
. 如果在年轻代和老年代一起执行则会被日志记录为[GC Pause (mixed)]
.
-
混合回收阶段
混合回收实际上与YGC是一样的:第一个步骤是从分区中选出若干个分区进行回收,这些被选中的分区称为Collect Set(简称CSet);第二个步骤是把这些分区中存活的对象复制到空闲的分区中去,同时把这些已经被回收的分区放到空闲分区列表中。垃圾回收总是要在一次新的YGC开始才会发生的。 -
G1 GC线程活动图
G1的full gc具体回收过程(标记整理)
- 并行标记活跃对象
- 计算对象的新地址
- 更新引用对象的地址
- 移动对象完成压缩
- 后处理
推荐使用 G1 的场景
- 堆内存在6GB及以上,主要针对配备多处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征
- Full GC 次数太频繁或者消耗时间太长.
- 对象分配的频率或代数提升(promotion)显著变化.
- 太长的垃圾回收或内存整理时间(超过0.5~1秒)
G1 vs 以前垃圾收集器
- CMS使用标记清除算法 产生空间碎片 G1采用复制算法不会造成内存碎片
- 对比Parallel Scavenge /Parallel Old 老年代基于标记整理算法 做清除空间和移动空间产生的GC停顿会比较长,而G1只是做特定整理几个region
ORACLE G1官方文档
G1 译文
ZGC
简介
Garbage Collector,也称为ZGC,是一种可扩展的低延迟垃圾收集器,旨在满足以下目标:
- 亚毫秒最大暂停时间
- 暂停时间不会随着堆、live-set 或 root-set 的大小而增加
- 处理大小从8MB到16TB的堆
工作步骤
- 初始标记
该步骤从根集合出发,找出根集合直接引用的活跃对象,并入栈;该步需要stw
- 并发标记
根据初始标记找到的对象,作为并发标记的根对象,使用深度优先遍历对象的成员变量进行标记;并发标记需要解决标记过程中引用关系变化导致的漏标记问题
- 再标记和非强根并行标记
在并发标记结束后尝试终结标记动作,理论上并发标记结束后所有待标记的对象会全部完成,但是因为GC工作线程和应用程序线程是并发运行,所以可能存在GC工作线程执行结束标记时,应用程序线程又有新的引用关系变化导致漏标记,所以这一步先判断是否真的结束了对象的标记,如果没有结束就还会启动并行标记,所以这一步需要STW。另外,在该步中,还会对非强根进行并行标记。
- 并发处理非强引用和非强根并发标记
在非强引用处理时对定义了finalize()函数 [1] 的对象需要特殊处理,为此ZGC设计了特殊的标记,另外,ZGC为了优化停顿时间,把一些需要在STW中并行处理的任务并发运行,这都被设计成非强根的并发标记。
-
重置转移集合中的页面
-
回收无效的页面
-
并发选择对象的转移集合,转移集合中就是待回收的页面。
-
并发初始化转移集合中的每个页面,
在后续重定位(也称为Remap)时需要的对象转移表(Forward Table)就是在这一步初始化的。
-
转移根对象引用的对象,该步需要STW
-
并发转移
把对象移动到新的页面中,这样对象所在的老的页面中所有活跃对象都被转移了,页面就可以被回收重用。”
Shenandoah回收器
Shenandoah 是低暂停时间的垃圾收集器,它通过在运行 Java 程序的同时执行更多的垃圾收集工作来减少 GC 暂停时间。Shenandoah 并发执行大部分 GC 工作,包括并发压缩,这意味着它的暂停时间不再与堆大小成正比。收集 200 GB 堆或 2 GB 堆的垃圾应该具有类似的低暂停行为。
- 支持并发的整理算法,Shenandoah的回收阶段可以和用户线程并发执行;
- Shenandoah 目前不使用分代收集,也就是没有年轻代老年代的概念在里面了;
- Shenandoah 摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题的发生概率
工作步骤
Shenandoah 是分区区收集器,它将堆维护为分区的集合。
- Init Mark 启动并发标记。
它为并发标记准备堆和应用程序线程,然后扫描根集。这是循环中的第一次暂停,最主要的消费者是根集扫描。因此,其持续时间取决于根集的大小。
- 并发标记
遍历堆,并跟踪可达对象。此阶段与应用程序一起运行,其持续时间取决于活动对象的数量和堆中对象图的结构。由于应用程序在此阶段可以自由分配新数据,因此在并发标记期间堆占用率上升。
- 最终标记
通过排空所有待处理的标记/更新队列并重新扫描根集来完成并发标记。它还通过确定要疏散的区域(集合集)来初始化疏散,预先疏散一些根,并通常为下一阶段准备运行时。部分工作可以在并发预清理阶段同时完成。这是循环中的第二次暂停,这里最主要的时间消费者是排空队列并扫描根集。
- 并发清理
回收即时垃圾区域 - 即在并发标记之后检测到的不存在活动对象的区域。
- 并发疏散
将对象从集合集中复制到其他区域。这是与其他 OpenJDK GC 的主要区别。此阶段再次与应用程序一起运行,因此应用程序可以自由分配。其持续时间取决于为循环选择的集合集的大小。
- 初始化更新引用阶段
除了确保所有 GC 和应用程序线程都已完成疏散,然后为下一阶段准备 GC 之外,它几乎什么都不做。这是循环中的第三次停顿,也是所有停顿中最短的一次。
- 并发更新引用
遍历堆,并更新对在并发疏散期间移动的对象的引用。 这是与其他 OpenJDK GC 的主要区别。 它的持续时间取决于堆中对象的数量,而不是对象图结构,因为它线性扫描堆。此阶段与应用程序同时运行。
- Final Update Refs
通过重新更新现有根集来完成更新引用阶段。它还从集合集中回收区域,因为现在堆没有对它们的(陈旧)对象的引用。这是循环中的最后一个暂停,其持续时间取决于根集的大小。
- 并发清理
回收现在没有引用的集合集区域。
GC停顿时间
像 Shenandoah 这样的并发 GC 隐含地依赖于比应用程序分配更快的收集速度。
如果分配压力很大,并且在 GC 运行时没有足够的空间来吸收分配,最终会发生分配失败。
Shenandoah 有一个优雅的降级阶梯
- Pacing 正常是小于10ms
- Degenerated GC 正常是小于100ms
- Full GC 正常是大于100ms
java -XX:+PrintCommandLineFlags -version
-XX:+PrintTLAB
如何衡量选取垃圾回收器
- 吞吐量 指在应用程序的生命周期内,应用程序所花费的时间和系统总运行时间(应用程序耗时+ GC耗时)的比值
- 停顿时间 垃圾回收器正在运行时,应用程序的暂停时间
- 应用的系统环境 1c1g 5c5g 64c128g 128c1T
- 垃圾回收频率 指垃圾回收器多长时间会运行一次
- 堆内存开销 指的是垃圾回收器需要的额外开销占堆内存的比例。
垃圾回收的演进方向
- 垃圾回收算法实现主要分为复制(Copy)、标记清除(Mark-
Sweep)和标记压缩(Mark-Compact)。 - 在回收方法上又可以分为串行回收、并行回收、并发回收。
- 在内存管理上可以分为代管理和非代管理。
参考资料
JVM G1源码分析和调优
JVM 垃圾回收
文档
b站视频
资料
x
CMS 中write barrier read barrier
G1垃圾收集器 - 三色标记法、漏标、增量更新、SATB
G1垃圾回收
Major GC和Full GC的区别是什么?触发条件呢?