作者: 老郑 2021-08-16 10:35:52
云计算
虚拟化 GC 垃圾回收器其主要的目的是为了实现内存的回收,在这个过程中主要的两个步骤就是:内存标记,内存回收。
成都创新互联坚持“要么做到,要么别承诺”的工作理念,服务领域包括:成都做网站、成都网站设计、企业官网、英文网站、手机端网站、网站推广等服务,满足客户于互联网时代的平原网站设计、移动媒体设计的需求,帮助企业找到有效的互联网解决方案。努力成为您成熟可靠的网络建设合作伙伴!
本文转载自微信公众号「运维开发故事」,作者老郑。转载本文请联系运维开发故事公众号。
GC 垃圾回收器其主要的目的是为了实现内存的回收,在这个过程中主要的两个步骤就是:内存标记,内存回收。
三色标记法,主要是为了高效的标记可被回收的内存块。
三色标记(Tri-color Marking)作为工具来辅助推导,把遍历对象图过程中遇到的对象,按照“是否访问过”这个条件标记成以下三种颜色:
标记过程:
什么是误标?当下面两个条件同时满足,会产生误标:
赋值器插入了一条或者多条黑色对象到白色对象的引用
赋值器删除了全部从灰色对象到白色对象的直接引用或者间接引用
要解决误标的问题,只需要破坏这两个条件中的任意一种即可,分别有两种解决方案:增量更新(Incremental Update) 和原始快照(Snapshot At The Beginning, STAB)
增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象 了。
原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删 除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描 一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。
对于错标其实细分出来会有两种情况,分别是:漏标和多标
如果标记执行到 E 此刻执行了 object.E = null
在这个时候, E/F/G 理论上是可以被回收的。但是由于 E 已经变为了灰色了,那么它就会继续执行下去。最终的结果就是不会将他们标记为垃圾对象,在本轮标记中存活。在本轮应该被回收的垃圾没有被回收,这部分被称为“浮动垃圾”。浮动垃圾并不会影响程序的正确性,这些“垃圾”只有在下次垃圾回收触发的时候被清理。还有在,标记过程中产生的新对象,默认被标记为黑色,但是可能在标记过程中变为“垃圾”。这也算是浮动垃圾的一部分。
给某个对象的成员变量赋值时,其底层代码大概长这样:
- /**
- * @param field 某个对象的成员属性
- * @param new_value 新值,如:null
- */
- void oop_field_store(oop* field, oop new_value) {
- *fieild = new_value // 赋值操作
- }
所谓写屏障,其实就是在赋值操作前后,加入一些处理的逻辑(类似 AOP 的方式)
- void oop_field_store(oop* field, oop new_value) {
- pre_write_barrier(field); // 写屏障-写前屏障
- *fieild = new_value // 赋值操作
- pre_write_barrier(field); // 写屏障-写后屏障
- }
当对象E的成员变量的引用发生变化时(objE.fieldG = null;),我们可以利用写屏障,将E原来成员变量的引用对象G记录下来:
- void pre_write_barrier(oop* field) {
- oop old_value = *field; // 获取旧值
- remark_set.add(old_value); // 记录 原来的引用对象
- }
【当原来成员变量的引用发生变化之前,记录下原来的引用对象】 这种做法的思路是:尝试保留开始时的对象图,即原始快照(Snapshot At The Beginning,SATB),当某个时刻 的GC Roots确定后,当时的对象图就已经确定了。比如 当时 D是引用着G的,那后续的标记也应该是按照这个时刻的对象图走(D引用着G)。如果期间发生变化,则可以记录起来,保证标记依然按照原本的视图来。值得一提的是,扫描所有GC Roots 这个操作(即初始标记)通常是需要STW的,否则有可能永远都扫不完,因为并发期间可能增加新的GC Roots。
SATB破坏了条件一:【灰色对象 断开了 白色对象的引用】,从而保证了不会漏标。
一点小优化:如果不是处于垃圾回收的并发标记阶段,或者已经被标记过了,其实是没必要再记录了,所以可以加个简单的判断:
- void pre_write_barrier(oop* field) {
- // 处于GC并发标记阶段 且 该对象没有被标记(访问)过
- if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
- oop old_value = *field; // 获取旧值
- remark_set.add(old_value); // 记录 原来的引用对象
- }
- }
当对象D的成员变量的引用发生变化时(objD.fieldG = G;),我们可以利用写屏障,将D新的成员变量引用对象G记录下来:
- void post_write_barrier(oop* field, oop new_value) {
- if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
- remark_set.add(new_value); // 记录新引用的对象
- }
- }
【当有新引用插入进来时,记录下新的引用对象】 这种做法的思路是:不要求保留原始快照,而是针对新增的引用,将其记录下来等待遍历,即增量更新(Incremental Update)。
增量更新破坏了条件二:【黑色对象 重新引用了 该白色对象】,从而保证了不会漏标。
- oop oop_field_load(oop* field) {
- pre_load_barrier(field); // 读屏障-读取前操作
- return *field;
- }
读屏障直接针对第一步 var objF = object.fieldG;,
- void pre_load_barrier(oop* field, oop old_value) {
- if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
- oop old_value = *field;
- remark_set.add(old_value); // 记录读取到的对象
- }
- }
- 这种做法是保守的
这种做法是保守的,但也是安全的。因为条件二中【黑色对象 重新引用了 该白色对象】,重新引用的前提是:得获取到该白色对象,此时已经读屏障就发挥作用了。
增量更新:CMS
原始快照(STAB):G1,Shenandoah
参考文档
https://www.jianshu.com/p/12544c0ad5c1
https://hllvm-group.iteye.com/group/topic/44381
https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html
https://tech.meituan.com/2016/09/23/g1.html
《深入理解 JVM 虚拟机-第三版》周志明
新闻名称:JVM三色标记法与读写屏障
路径分享:http://www.shufengxianlan.com/qtweb/news28/297078.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联