Java JVM 4:CMS 垃圾收集器 - 工作原理,浮动垃圾,三色标记法等

CMS 全称为 Concurrent Mark Sweep。它是现在非常主流的一款老年代的垃圾回收器,因为它能够实现和用户线程并行进行,而不需要像其他的垃圾收集器一样(如 Serial Old,Parallel Old) “stop the world”。

工作原理

主要分为这几步:

初始标记
并发标记
重新标记
并发清除

在这四步中,其实初始标记和重新标记都是属于 “stop the world” 的,但是由于这两个过程很短,然后呢,并发标记和并发清除这两个过程相对长一点,而且刚好这两个过程又可以和用户线程一起并发执行,所以说,可以把 CMS 近似看做是不需要 “stop the world”(但是实际上有两步还是需要的)

以下有些内容也参考《垃圾回收的算法与实现》:

初始标记

初始标记也就是标记一下 GC roots 关联到的对象。(并不是所有活动对象)

并发标记

并发标记就需要标记出 GC roots 关联到的对象 的引用对象有哪些。比如说 A -> B (A 引用 B,假设 A 是 GC Roots 关联到的对象),那么这个阶段就是标记出 B 对象, A 对象会在初始标记中标记出来。

这个过程是可以和用户线程并发执行的。所谓的并发的实现,可以有几种方式,比如说,标记了 100 个对象,那么就停一停,让用户线程跑一会;再比如说,标记了 10ms,再停一停,之类的实现。

浮动垃圾问题

如果深入思考,可能会有这样的疑问,假如你标记了一个对象,然后用户线程说,这个对象我不要了,你回收吧。这个时候怎么办?其实这个时候暂时没办法处理,只能留到下一次 GC 的时候再回收,这次 GC 不好意思,GC 不了,这个就叫做浮动垃圾。

这个其实还好,GC 不了对程序不会有影响,大不了多占用了一点内存嘛,反正下次也释放。

但是还可能有这样的问题出现:

假如有一个对象 GC 线程没有标记(用户线程之前没在用),然后轮到了用户线程,用户线程说,这个对象我重新又要用了,不要把这个对象GC 掉,这个时候怎么办?假如这个时候处理不了,还是 GC 了,那么程序就直接报错了,这个是不允许的,解决办法可以如下:

三色标记法

这个算法就是把 GC 中的对象划分成三种情况:

白色:还没有搜索过的对象(白色对象会被当成垃圾对象)
灰色:正在搜索的对象
黑色:搜索完成的对象(不会当成垃圾对象,不会被GC)
上面说的这个问题就是可以用下图来表示:

这里写图片描述

这个图的意思就是:假设有 A -> B -> C, A 是 GC Roots 关联的对象,那么首先会把 GC Roots 标记,也就是 A 标记成灰色(证明现在正在搜索 A 相关的),然后搜索 A 的引用,也就是 B,那么搜索了 B,把 B 变成了灰色,那么 A 就搜索完成了。(此时注意,现在是不管 C 的,因为 C 不是 A 的引用,现在只管 A 的引用是什么)。此时把 A 相关的搜索完了,那么 A 就变成了黑色,证明 A 已经 ok 了。

(Ps:浮动垃圾就是说,此时 A 又不用了,那么 A 是没办法回收的,因为 A 已经标记了)

此时准备要搜索 B 了。

刚好,此时,用户线程要执行了,用户线程把原来 A -> B -> C 的引用改成了 A -> C,同时 B 不再引用C。

然后又到 GC 线程执行了。

GC 线程发现 B 没有引用的对象了(因为用户线程已经把 B -> C 去掉了),那么 B 就相当于搜索完成了,变成黑色了。

最后,C 怎么办,C还是白色的呢,白色的是不会搜索,当做垃圾处理的。

解决办法

此时的解决办法就是有一个叫做写入屏障的东西。就是说,如果A已经被标记了(已经是黑色的了),那么用户线程改动 A->C的时候,会把 C 变成灰色,这样,以后就可以搜索 C了。

例如:

write_barrier(obj,field,newobj){
    if(newobj.mark == FALSE){
        newobj.mark = TRUE
        push(newobj,$mark_stack)
    }
    *field = newobj
}

这里的意思就是改的时候会 push(newobj,$mark_stack),

那么,通过这样子的一个方式,就能解决这个问题:GC 线程和 用户线程并发的时候,用户线程把失效的对象又至为有效,这个时候怎么处理。

重新标记

重新标记是干什么的呢?就是由于并发标记这个阶段用户线程和GC 线程并发,假如这个阶段用户线程产生了新的对象,这个对象是白色的,总不能被 GC 掉吧。这个阶段就是为了让这些对象重新标记。

(Ps:上面说的三色标记法是指针对原来已经有了的对象,重新标记是针对原来没有的对象)

伪代码例如:

一开始需要初始标记,那么就:

root_scan_phase(){
    for(r : $roots){
        mark(*r)
    }
    $gc_phase = GC_MARK
}

然后并发标记,重新标记:

incremental_mark_phase(){
    for(i : 1 ... MARK_MAX){
        if(is_empty($mark_stack) == FALSE){
            obj = pop($mark_stack)
            for(child : children(obj)){
                mark(*child)
            }
        } else {
            for(r: $roots){
                mark(*r)
            }
            whlie(is_empty($mark_stack) == FALSE){
                obj = pop($mark_stack)
                for(child : children(obj)){
                    mark(*child)
                }
            }
        }
    }

    $gc_phase = GC_SWEEP
    $sweeping = $heap_start
    return
}

这里可以看到,for(i : 1 … MARK_MAX) 是实现用户线程和 GC 线程并发的一个手段,就是说标记了 MARK_MAX 次,然后就换用户线程跑一跑。$mark_stack 装的其实就是灰色的对象。

这里可以看到 ,如果没有灰色对象了,也就是说全部都标记完了,那么就会走到 else 语句里,else 语句还要进行一次:

for(r: $roots){
    mark(*r)
}

为什么呢,一开始不就进行了一次? 其实这里就是重新标记了,为了再遍历一次并发标记的时候用户线程产生的新对象。

并发清除

那就清除了呗。因为采用的是标记-清除算法,所以可以参考:第3章:垃圾收集算法 - 标记清除算法(伪代码实现与深入分析)

另外一个问题:Concurrent Mode Failure

由于其他垃圾回收器都是 “stop the world”,那么内存不够了就执行 GC。但是 CMS 垃圾回收器是可以和用户线程一起并发的,那么内存使用率到了 50%就收集?还是70%?90%?因为 CMS 需要留一些内存给并发的时候用户线程使用。

比如说留了 10%,假设这个时候用户线程说,10% 不够,这个时候怎么办?这个时候就报 Concurrent Mode Failure、这个时候就会有预备方案,Serial Old 垃圾回收器会替代 CMS,进行 “stop the world”垃圾收集。

所以说,CMS 一般都是搭配 Serial Old 来用于老年代。

最后

由于采用了标记-清除算法,所以难免会有内存碎片的问题,分配新对象的时候遍历空闲链表等这些问题就来了。根据 《深入理解 Java 虚拟机》,CMS 提供了一些参数,用于进行压缩(也就是参考: 第3章:垃圾收集算法 - 标记整理算法(伪代码实现与深入分析))。如:-XX:+UseCMSCompactAtFullCollection ,-XX:CMSFullGCBeforeCompaction

  • 8
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值