CMS GC 发生 concurrent mode failure 时的 full GC 为什么是单线程的?
以下的回答来自 R 大。
因为没足够开发资源,偷懒了。就这么简单。没有任何技术上的问题。 大公司都自己内部做了优化。
所以最初怎么会偷这个懒的呢?多灾多难的CMS GC经历了多次动荡。它最初是作为Sun Labs的Exact VM的低延迟GC而设计实现的。
但 Exact VM在与 HotSpot VM争抢 Sun 的正牌 JVM 的内部斗争中失利,CMS GC 后来就作为 Exact VM 的技术遗产被移植到了 HotSpot VM上。
就在这个移植还在进行中的时候,Sun 已经开始略显疲态;到 CMS GC 完全移植到 HotSpot VM 的时候,Sun 已经处于快要不行的阶段了。
开发资源减少,开发人员流失,当时的 HotSpot VM 开发组能够做的事情并不多,只能挑重要的来做。而这个时候 Sun Labs 的另一个 GC 实现,Garbage-First GC(G1 GC)已经面世。
相比可能在长时间运行后受碎片化影响的 CMS,G1 会增量式的整理/压缩堆里的数据,避免受碎片化影响,因而被认为更具潜力。
于是当时本来就不多的开发资源,一部分还投给了把G1 GC产品化的项目上——结果也是进展缓慢。
毕竟只有一两个人在做。所以当时就没能有足够开发资源去打磨 CMS GC 的各种配套设施的细节,配套的备份 full GC 的并行化也就耽搁了下来。
但肯定会有同学抱有疑问:HotSpot VM不是已经有并行GC了么?而且还有好几个?
让我们来看看:
- ParNew:并行的young gen GC,不负责收集old gen。
- Parallel GC(ParallelScavenge):并行的young gen GC,与ParNew相似但不兼容;同样不负责收集old gen。
- ParallelOld GC(PSCompact):并行的full GC,但与ParNew / CMS不兼容。
所以…就是这么一回事。
HotSpot VM 确实是已经有并行 GC 了,但两个是只负责在 young GC 时收集 young gen 的,这俩之中还只有 ParNew 能跟 CMS 搭配使用;
而并行 full GC 虽然有一个 ParallelOld,但却与 CMS GC 不兼容所以无法作为它的备份 full GC使用。
为什么有些新老年代的收集器不能组合使用比如 ParNew 和 Parallel Old?
这张图是 2008 年 HostSpot 一位 GC 组成员画的,那时候 G1 还没问世,在研发中,所以画了个问号在上面。
里面的回答是 :
"ParNew" is written in a style... "Parallel Old" is not written in the "ParNew" style
HotSpot VM 自身的分代收集器实现有一套框架,只有在框架内的实现才能互相搭配使用。
而有个开发他不想按照这个框架实现,自己写了个,测试的成绩还不错后来被 HotSpot VM 给吸收了,这就导致了不兼容。
我之前看到一个回答解释的很形象:就像动车组车头带不了绿皮车厢一样,电气,挂钩啥的都不匹配。
新生代的 GC 如何避免全堆扫描?
在常见的分代 GC 中就是利用记忆集来实现的,记录可能存在的老年代中有新生代的引用的对象地址,来避免全堆扫描。
上图有个对象精度的,一个是卡精度的,卡精度的叫卡表。
把堆中分为很多块,每块 512 字节(卡页),用字节数组来中的一个元素来表示某一块,1表示脏块,里面存在跨代引用。
cms 中需要记录老年代指向年轻代的引用,但是写屏障的实现并没有做任何条件的过滤。
即不判断当前对象是老年代对象且引用的是新生代对象才会标记对应的卡表为脏。
只要是引用赋值都会把对象的卡标记为脏,当然YGC扫描的时候只会扫老年代的卡表。
这样做是减少写屏障带来的消耗,毕竟引用的赋值非常的频繁。
那 cms 的记忆集和 G1 的记忆集有什么不一样?
cms 的记忆集的实现是卡表即 card table。
通常实现的记忆集是 points-out 的,我们知道记忆集是用来记录非收集区域指向收集区域的跨代引用,它的主语其实是非收集区域,所以是 points-out 的。
在 cms 中只有老年代指向年轻代的卡表,用于年轻代 gc。
而 G1 是基于 region 的,所以在 points-out 的卡表之上还加了个 points-into 的结构。
因为一个 region 需要知道有哪些别的 region 有指向自己的指针,然后还需要知道这些指针在哪些 card 中。
其实 G1 的记忆集就是个 hash table,key 就是别的 region 的起始地址,然后 value 是一个集合,里面存储这 card table 的 index。
我们来看下这个图就很清晰了。
像每次引用字段的赋值都需要维护记忆集开销很大,所以 G1 的实现利用了 logging write barrier(下文会介绍)。
也是异步思想,会先将修改记录到队列中,当队列超过一定阈值由后台线程取出遍历来更新记忆集。
为什么 G1 不维护年轻代到老年代的记忆集?
G1 分了 young GC 和 mixed gc。
young gc 会选取所有年轻代的 region 进行收集。
midex gc 会选取所有年轻代的 region 和一些收集收益高的老年代 region 进行收集。
所以年轻代的 region 都在收集范围内,所以不需要额外记录年轻代到老年代的跨代引用。