zoukankan      html  css  js  c++  java
  • Mark Sweep GC

    标记清除算法

    GC 标记-清除算法是由标记阶段清除阶段构成。标记阶段把所有活动的对象做上标记。清除阶段是吧没有标记的对象也就是非活动的对象进行回收。通过这两个阶段就可以对空间进行重新利用。

    mark_sweep()函数

    mark_sweep(){
        mark_phase()  //标记
        sweep_phase() //清除
    }
    

    在执行GC前堆的状态

    标记阶段

    使用 mark_phase()函数进行处理。在标记阶段,collector会为堆里所有活动的对象打上标记。我们首先要通过根找到直接引用的对象进行标记,之后才能递归的访问到对象并把所有活着的对象进行标记。

    mark_phase()

    mark_phase(){
        for(r:$roots)
            mark(*r)  //从根开始取它的孩子进行标记
    }
    
    mark(obj){
        if(ob.mark == FALSE)
            obj.mark = TRUE  // 如果还是标记是FALSE则改为TRUE
            for(child :children(obj)) // 再去递归的标记自己的孩子
                mark(*child)
    }
    

    标记完所有对象后,标记活动就结束了。这时,状态如下图所示。

    在标记阶段中,程序会标记所有活动对象,毫无疑问,对象越多需要的时间就越多。他们是正相关的。

    深度优先于广度优先

    这时一个老生常谈的问题了。应用很广,因为像这种类型可能没有其他方式遍历了,其使用的队列和递归也是非常常用的。

    深度优先depth-first search 使用递归实现

    广度优先breadth-first search 使用队列实现

    无论使用哪种方法,搜索的对象时不会变的。但是深度优先可以压低内存的使用量,因此我们经常实用深度优先。

    清除阶段

    在清除阶段,cllector会遍历整个堆。回收垃圾,使其能再次利用。此时出现了size的域这是存储对象大小的域,同mark一样我们在头中定义了它。简单的说,在块的头中现在有mark和size。mark表示时候有用,size表示块的大小。
    sweep_phase()

    sweep_phase(){
        sweeping = $heap_start  //堆头
        while(sweeping < $heap_end)  //现在为止在堆尾之前
            if(sweeping.mark == TRUE)
                SWEEPING.mark = FALSE //这里标记为false。因为如果以后不再使用了她就能被回收,否则就不能回收了。
            else
                sweeping.next = $free_list //初始化空链表
                $free_list = sweeping // 连接到一个free链表上
                
            sweeping += sweeping.size //可以看做是基址+偏移。拿到下一个块地址
    }
    

    通过上面的操作,我们拿到了free_list。之后我们针对它进行分配空间操作。下图就表现出gc后两个链表

    清除阶段同理,会遍历所有块进行垃圾回收。时间和堆的大小成正相关。

    分配

    那么对于垃圾回收的再利用是怎么进行的呢?。当mutator申请分块时,怎样才能分呢?

    我们使用nwe_obj函数来分配。

    new_obj

    nwe_obj(){
        chunk = pickup_chunk(size, $free_list)  // 我需要的大小size 和查找对象 free_list
        if (chunk !=NULL)
            return chunk  //拿到了
        else
            allocation_fail()  // 拿不到
    }
    

    在分配的时候有几个点:不能分配比size小的,只能使用大于等于size的块去进行分配,如果大小合适再好不过,如果大了就需要去分割一个和size一样大小的块再分配,剩余部分返回空链表。

    First-fit、Best-fit、Worst-fit三种分配策略

    • First-fit:最初发现大于等于size的分块就进行操作。可能第一个就比size大,但是第二个就和size一样大。这样就直接分了第一个,碎片严重。
    • Best-fit:遍历空链表,选择最合适的。性能低啊。
    • Worst-fit:找出空闲链表中最大的块,分割成size和其他。也容易产生碎片,比第一个还容易,不推荐使用。

    综上, 考虑到碎片和性能问题,选择First-fit还是比较明智的选择。

    合并

    根据分配策略会生成大量小块,但是如果小块是连续的我们其实可以将它连接起来成为一个大块。这就叫做合并coalescing

    sweep_phase()

    sweep_phase(){
        sweeping = $heap_start  //堆头
        while(sweeping < $heap_end)  //现在为止在堆尾之前
            if(sweeping.mark == TRUE)
                SWEEPING.mark = FALSE //这里标记为false。因为如果以后不再使用了她就能被回收,否则就不能回收了。
            else
                if(sweeping = $free_list+$free_list.size)// 先验证是不是相邻的。
                    $free_list.size +=sweeping.size  //这样就连接起来了。
                else
                    sweeping.next = $free_list //初始化空链表
                    $free_list = sweeping // 连接到一个free链表上
            
            sweeping += sweeping.size //可以看做是基址+偏移。拿到下一个块地址
    }
    

    优点

    实现简单

    他其实就是用到了标识位,然后进行遍历。算法上难度很低。不像其他算法难度是比较大的。

    与保守式GC算法兼容

    在保守式GC中,对象时不能被移动的。例如复制算法他就是将数据复制过来,在复制过去,是移动的。而标记清除不移动对象,所以非常适合保守式GC算法。实际上很多采用保守式gc的程序中使用到了标记清除。

    缺点

    碎片化

    算法使用过程中产生被细化的分块,在不久后就会散落在各处。我们将这种状况称为碎片化(fragmentation)。windows文件系统也会有这种现象。

    如果发生了碎片化,及时空闲链表再大,也不能分配成功。为了解决这个问题可以采用压缩,但是本文中介绍了一下BIBOP法提供参考。

    分配速度

    标记清除算法中分块不是连续的,因此每次都需要遍历链表。在最糟糕的情况下是每次是链表的最后一块。因此速度非常慢,如果一个大型游戏采用这种方式后果可想而知。

    后文叙述的多个空闲链表和BIBOP都是为了解决速度而采取的方案。

    与写时复制技术不兼容

    写时复制技术(copy-on-write)是在Linux等众多unix操作系统的虚拟存储中使用的高速化方案。例如在Linux复制进程使用fork()时,大部分空间都不会被复制。如果说为了复制进程就复制了所有空间这样内存怎么多也不够。因此写时复制技术就假装复制了内存空间实际上是共享内存空间

    当然我们对共享的空间写入时不能直接写它,因为它是别的。这时候就要将其复制到自己的私有空间然后进行重写。复制后之访问私有空间不访问共享空间。

    但是标记清除算法,就算是没有重写,也会进行不断的复制。实际上我们还是希望它是用共享,而不是浪费内存。为了处理这个问题我们采用为图标记法bitmap marking。

    多个空闲链表

    之前的算法中使用的只有一个空闲链表,对大块和小块进行统一的处理。这样一来无论大小都要遍历很长的链。

    因此我们对块进行分类。大的是一组,小的是一组。这样一来按照mutator所申请的空间大小选择合适的块就容易的多。

    一般情况来说,mutator很少会申请非常大的分块。为了应对这种极少数情况,我们给分块设定一个上限。如果分块大于等于这个大小,就全部采用一个空链表。

    利用多个空链表时,我们需要修正new_obj()以及sweep_phase()

    new_obj

    nwe_obj(){
        index = size/(WORD_LENGTH/BYTE_LENGTH)  //根据商来选择合适的建表
        if (index <=100 )  //小于100字
            if($free_list[index] !=NULL)
                chunk = $free_list[index] //  获取链的索引,分给他一个index字链上的第一块
                $free_list[index] = $free_list[index].next
                return chunk
        else  //大于100字
            chunk = pickup_chunk(size, $free_list[101])  // 我需要的大小size 和 查找对象 free_list
            if (chunk !=NULL)
                return chunk  //拿到了
            else
                allocation_fail()  // 拿不到
    }
    

    sweep_phase()

    sweep_phase(){
        for(i:2...101)
            $free_list[1] = NULL
            
        sweeping = $heap_start  //堆头
        while(sweeping < $heap_end)  //现在为止在堆尾之前
            if(sweeping.mark == TRUE)
                SWEEPING.mark = FALSE //这里标记为false。因为如果以后不再使用了她就能被回收,否则就不能回收了。
            else
                index = size/(WORD_LENGTH/BYTE_LENGTH)
                    if(index <=100)
                        sweeping.next = $free_list[index]
                        $free_list[index] = sweeping
                    else
                        sweeping.next = $gree_list[101]
                        $free_list[101] = sweeping
                        
            sweeping += sweeping.size //可以看做是基址+偏移。拿到下一个块地址
    }
    

    BIBOP法

    BiBOP是Big Bag Of Pages的缩写,就是将大小相近的对象整理成固定大小的块进行管理的做法。我们可以使用这个方法把堆分成固定大小的块,让每个块只能配置同样大小的对象这就是BiBOP算法。不觉得内存利用效率低吗?熊die。

    如图所示,在多个分块中残留同样大小的对象反而会使堆的使用效率低下。

    位图标记

    在以前的标记清除算法中,标记是在对象头中的,这样造成了与写时复制技术的不兼容。对此收集头部标识表格化将标识与对象分开管理。这样的标记方法称为位图表格(bitmap table)。位图标记可以采用如散列表,树形结构等。为了简单起见我们使用数组。如下图。

    表中的位置和堆里的对象一一对应。一般来说堆中的一个字会分到一个位。

    mark()

    mark(obj){
        obj_num = (obj-$heap_start)/WORD_LENGTH
        index = obj_num / WORD_LENGTH
        offset = obj_num % WORD_LENGTH
        if (($bitmap_tbl[index]&(1<<offset))==0)
            $bitmap_tbl[index] != (1<<offset)
            for (child :children(obj))
                mark(*child)
    }
    

    这里WORD_LENGTH 是个常量,表示各机器中1个字的位宽。obj_num指从位图表格前面数起,obj的标志位在第几个。例如上图中E,它的obj_num就是8.但是bitmap的图是从后往前的所以E的标志位应该从右往左数是第九个位。如下图

    优点

    与写时复制技术兼容

    以往标记位是对对象进行设置,而位图标记不对对象进行设置采用了映射或许可以理解为引用。所以自然不会发生无谓的复制。

    清除操作更高效

    利用位图表格的清除操作把所有对象的标志位集合到一处,可以快速定位。与一般的清除阶段相同,我们sweeping遍历整个堆,不过这里使用了index和offset两个变量,在遍历堆的同时也遍历位图表格。

    sweep_phase

    sweep_phase(){
        sweeping = $heap_start
        index = 0
        offset = 0
        while(sweeping <$heap_end)
            if($bitmap_tb1[index] &(1<<offset) ==0)
                sweeping.next = $free_list
                $free_list = sweeping
            index +=(offset + sweeping.size) /WORD_LENGTH
            offset = (offset + sweeping.size) % WORD_LENGTH
            sweeping += sweeping.size
        for (i:0...(HEAP_SIZE/WORD_LENGTH-1))
            $bitmap_tbl[i] = 0
    }
    

    注意

    对象地址和位图表格对应。通过对象的地址求与其对应的位置标志,要进行位运算的。再有多个堆且地址不连续的情况下,必须采用多个表。即每个堆一个表。

    延迟清除法

    我们之前说过,清除花费的时间和堆的大小成正比。这样一来堆越大就越影响mutator。会越慢。

    延迟清除(Lazy Sweep)是缩减因清除操作而导致的mutator最大暂停时间的方法。在标记操作结束后,不一并进行清除。通过延迟来防止mutator长时间暂停。

    nwe_obj

    nwe_obj(size){
        chunk = lazy_sweep(size)
        if (chunk!=NULL)
            return chunk
        mark_phase()
        chunk = lazy_sweep(size)
        if(chunk !=NULL)
            return chunk
        allocation_fail()
        
    }
    

    分配时调用lazy_sweep进行清除。如果他能用清除操作来分配块,就会返回分开,如果不能分配块就会执行标记操作。当lazy_sweep函数返回NULL时就是没有找到。那就在进行一遍此操作。如果还没能分得代表没有分块。mutator也就不需要进行下一步处理。

    lazy_sweep

    lazy_sweep(size){
        while($sweeping <heap_end)
            if($sweeping.mark == TRUE)
                $sweeping.mark = FALSE
            else if($sweeping.size >=size)
                chunk = $sweeping
                $sweeping +=$sweeping.size
                return chunk
            $sweeping += $sweeping.size
        $sweeping = $heap_start
        return NULL
            
    }
    
    

    次函数会一直遍历堆,知道找打大于等于所申请的空间。再找到时候会将其返回,但是$sweeping是全局变量,也就是说遍历开始位置谓语上一次清除操作中发现的分块右边。

    当次函数没有找打分块时候会返回NULL。

    此方法在分配时执行必要的遍历,因此可以压缩清除操作导致mutator暂停的时间,这就是延迟的意思。

    只有延迟清除是不够的

    虽然可以减少mutator的暂停时间。但是延迟清除的效果是不均匀的。打个比方如下图。

    垃圾变成了垃圾堆,活动对象变成了活动对象堆。这种情况下,程序清除垃圾较多的部分时马上就能获得分块,随意能减少mutator的暂停时间。然鹅一旦程序开始清除活动对象周围就怎么也获得不了分块,这就增加了mutator的暂停时间。

    至于有什么其他方法。后续会在其他文章里。

  • 相关阅读:
    解析大型.NET ERP系统 权限模块设计与实现
    Enterprise Solution 开源项目资源汇总 Visual Studio Online 源代码托管 企业管理软件开发框架
    解析大型.NET ERP系统 单据编码功能实现
    解析大型.NET ERP系统 单据标准(新增,修改,删除,复制,打印)功能程序设计
    Windows 10 部署Enterprise Solution 5.5
    解析大型.NET ERP系统 设计异常处理模块
    解析大型.NET ERP系统 业务逻辑设计与实现
    解析大型.NET ERP系统 多国语言实现
    Enterprise Solution 管理软件开发框架流程实战
    解析大型.NET ERP系统 数据审计功能
  • 原文地址:https://www.cnblogs.com/Leon-The-Professional/p/9950179.html
Copyright © 2011-2022 走看看