Yak是专门为分布式计算场景产生的JVM GC,主要思想在于将heap分为控制和数据区域。分布式计算中控制面和数据面通常泾渭分明,例如集群管理和实际运算往往是分离的,而他们的对象生命周期模式却有着很大差别。
Reference:Yak: A High-Performance Big-Data-Friendly Garbage Collector
Two Paths
- 对于控制面,大部分对象的生命周期往往是短暂的,和传统程序类似。
- 对于数据面,一组数据对象(如图计算中的节点和边)却往往同生共死,贯穿某个时期。
Two Hypotheses
传统的GC通常采用分代假说,初始时所有对象都在young,并在minor GC发生时把那些card table可达的young扔进old,对old代的GC则不频繁(例如G1 GC)。
然而在分布式计算下假说却不适用,移动young对象非常浪费时间。有人提出了新的假说epochal hypothesis,意思是数据有着相同的生命周期,都在epoch(时代)末尾消亡。
传统的GC如果放在分布式计算下就会发现时间开销比较大。
Yak GC
Yak的思想在于分离两个独立的区域CS和DS,两个区域内分别按照不同的假说执行对应的GC算法,作为混合GC。然而混合并非那么简单
- CS和DS存在引用,例如分代GC应该无视CS指向DS的引用,又需要注意DS指向CS的引用。
- DS区域需要正确管理,例如逃逸到其他epoch或者CS,不能一同消亡。
- DS的管理需要正确的同时保证效率,不能简单地监控所有堆访问。
设计
epoch可以通过hadoop中的setup/cleanup进行界定,为了跨框架,作者用@epoch_start和@epoch_end注解作为定界符,并且epoch是可以嵌套的。
同epoch的所有对象在epoch开始时分配在一个region中,并在epoch结束时销毁。每个线程都各自拥有该epoch的region。
在epoch结束时,为了避免GC时访问异常需要进行短暂的STW,Yak需要确定逃逸对象并且将他们分配在新的region里,逃逸对象需要被迁移到引用它的所有对象的公共region上界,如果是不同线程之间的引用就只能迁移到CS。
Allocation
最大堆大小、CS/DS比例、region大小均为虚拟机参数。
CS的RS记录所有活对象
DS的RS记录逃逸对象以及如何安置,实质上是HashMap<Object,Set<Reference>>,这里没有用card table的原因在于,card table只能确定是否有引用,而不能确定引用的是谁,因此需要对整个CS进行查找。
当分配请求发生在epoch内部时,原本对Eden的分配会被重定向到DS上。这里利用bump pointer机制,维护一个始终指向末尾page的指针,如果page内存不够会创建新的页,对于大对象则会创建特殊页,特殊页不会被移动。
跨Region引用
- 每个对象头部增加4字节的re记录当前region ID,CS具有特殊的ID
- 改变写屏障算法,写屏障会在每次对堆写时执行(所以Java这性能怎么高的起来),在传统GC中写屏障是用来记录跨代引用的,这里维护了跨region引用。
3. epoch结束时,再记录栈上或者其他线程栈上的引用
堆
Remember Set内部存储着A.f->B的引用,通过上面的算法维护,问题在于A可能是栈对象,因此这里使用占位符,表示A所在的栈。这里检查了是否两者都在CS以免除overhead。
栈
如果被栈引用,那么对象就逃逸了,这个过程是在end的region deallocation时分析的。这里事实上做了个迁移,把栈引用的对象标记到父Region上进行逃逸。
逃逸对象需要被迁移到引用它的所有对象的公共region上界
远程栈
- 移动对象 - 如果移动对象,v依然指向原地址
- 回收对象 - E被回收,则v为空悬指针
因此Yak的做法是在GC时暂停其他线程,扫描其他的栈,发现远程栈引用。
Deallocation
逃逸分析
有三种引用导致逃逸,分别是跨Region、Stack、Remote Stack。因此这里的一开始就做了标记,把引用都加到RS里。
闭包计算
把所有导致逃逸的引用所在的Region进行join-semilattice。
简单理解成上面那张Region的树形图就好了,只不过不一定是直系子Region
按照拓扑序遍历Region,做BFS,迁移到Target Region
逃逸对象需要被迁移到引用它的所有对象的公共region上界
目标确认
如果同时被多个Region引用,需要找出最高层的Region,这里用了比较巧妙地做法从而只需要遍历一次树,因为是从高往低的BFS,所以能直接迁移到最高层。
更新RS与对象迁移
Region内部的引用
通过forward reference(因为同region之间不构成逃逸引用,因此需要特殊维护),这样能够维护source的引用关系(例如A->B,当B位置变化时,我需要告诉A新的地址)
Region外部的引用
直接修改RS中记录的跨Region引用即可,这里需要注意的是如果是同Region则不能记录在RS里,因为不构成逃逸引用。
栈对象的引用修改同理,虽然是占位符但是机制相同,source不同而已。
还需要重新检查一遍引用,因为所在的region改变了,原本的Region内部引用现在变成了外部引用需要记录在RS里。
最后清除所有当前region的RS,因为当前region所有活对象已迁出。
GC结果如上
CS GC
Parallel Scavenge GC的变种,首先,如果reference是DS的对象则忽视;然后把CS的RS进行Tracing。在Tracing前,需要validate it by comparing the address of its target CS object with the current content in its source location,否则引用失效(没太理解)
因为Parallel Scavenge GC需要迁出年轻代对象,所以也要维护引用关系(forward reference)
作者省略了对remember set layout, large object allocation, as well as region/thread ID lookup的优化
总结
Problem: DS下分代假说不成立,需要用划时代假说(啊这,这咋翻译嘛)
Related work: 仅仅用了单一GC,无法同时handle CS和DS
Observation: Hybrid GC
Solution: CS和DS采用不同GC策略,通过写屏障维护Remember Set进行逃逸分析
Evaluation: 性能UPUP,专为分布式计算而生
Comments: 侵入性太强,需要修改应用程序