Conservative GC (Part two :MostlyCopyingGC )

MostlyCopyingGC

Mostly Copying GC, Joel F.Bartlett, 1989

此算法可以在不明确根的环境中运行GC复制算法。

概要

Mostly Copying GC就是“把不明确的根指向的对象以外的对象都进行复制”,抛开那些不能移动的对象将其他大部分的对象进行复制的GC算法。

  • 前提
    • 根是不明确的根
    • 没有不明确的数据结构 :可以明确判断对象里的域是指针还是非指针
    • 对象大小随意

堆结构

下图所示,是 Mostly Copying GC的堆结构,堆被分成一定大小的页(page),每个页都有编号。那些没有分配到对象的空页则有一个$current_space以外的编号。页编号不能造成数据溢出。

GC时,$current_space 和 $next_space用于识别To空间和From空间。编号和$next_space一样的是To,编号和$current_space一样的是From页面。一般情况下$current_space和$next_space是同一个值,只有在GC时才会不同。$current_space的值会被分配到装有对象的正在使用的页。如上图示,正在使用的页编号为1.

此外,我们要为正在使用的页设置一下两种标志的一种。

  • OBJECT : 正在使用的页
  • CONTINUED :当正在使用的页跨页时,设置在第二个页之后。

以上两个标识不是一块排列在内存当中,因为会出现跨页分配对象,所以从实现上来说,我们必须把页和标识分配在不同的内存位置进行管理。

分配

根据分块的大小、分配对象的大小不同,分配的动作也各不相同。

  • 如果正在使用的页里有符合mutator申请大小的分块,对象就会被分到这个页。如下图示:

  • 当正在使用的页没有合适大小的分块时,对象就会被分配到空的页,然后正在使用的这个新页会被设置OBJECT标识。

  • 当mutator申请分配超过页大小时。分配程序会将对象夸多个页来分配。和平时一样,开头页设定OBJECT,之后的页设定CONTINUED。如下图示:

new_obj()函数

分配的伪代码

new_obj(size){
    while(size > $free_size) // $free_size用来保持分块大小
        $free_size = 0 
        add_page(byte_to_page_num(size))
    
    obj = $free
    obj.size = size
    
    if(size < PAGE_SIZE)
        $free_size -= size
        $free += size
    else
        $free_size = 0
        
    return obj
}
  • 将要申请的大小size自己传给new_obj。
  • 判断size和$free_size大小。如果$free_size小于size 那么add_page()函数会分配新的页,扩大分块大小。然后新分配的页数会传递给add_pages()函数。
  • $free指向分块开头, 将obj设置为$free
  • 判断size是否小于$PAGE_SIZE。当size小于页大小时,则会从$free_size中减去size。也就是当前页剩余的大小。然后指针$free向后移动size。
  • 如果size大于$PAGE_SIZE。$free_size变为0。这样一来对象就不会被分到CONTINUED页了。

add_pages()函数

负责重新分页的add_pages()函数。

add_pages(page_num){
    if($allocated_page_num + page_num >= HEAP_PAGE_NUM/2)
        mostly_copying()
        return 
    
    first_free_page = find_free_pages(page_num)
    
    if(first_free_page == NULL)
        allocation_fail()
    
    if($next_space != $current_space)
        enqueue(first_free_page, $to_space_queue)
    
    allocate_page(first_free_page, page_num)
}

  • $allocated_page_num 表示正在使用的页数,HEAP_PAGE_NUM表示堆中的总页数。如果出现“正在使用的页数+准备追加的页数>总页数的一半”,这种情况下启动GC mostly_copying()。
  • first_free_pages()函数,在堆内寻找连续的page_num个空页。如果有则返回最开头的空页指针,否则返回NULL表示失败。
  • 当运行GC复制对象时,为了使用new_obj() 函数,也会在GC里调用这个add_ pages() 函数。另外,第三个if的条件只有在 GC 里才为真。之后会把GC中分配的页连接上$to_space_queue。当GC执行时,这里连接上$to_space_queue的页相当于To空间。
  • 最后一行是对于找到连续page_num的指针调用allocate_pages()方法,申请空间。
allocate_pages(first_free_page, page_num){
    $free_page = first_free_page
    $free = first_free_page
    $free_size = page_num*PAGE_SIZE
    $allocated_page_num += page_num
    
    set_space_type(first_free_page, $next_space)
    set_allocate_type(first_free_page, OBJECT)
    
    while(--page_num > 0)
        $free_page = next_page($free_page)
        set_space_type($free_page, $next_space)
        set_allocate_type($free_page, CONTINUED)
    $free_page = next_page($free_page)
    
}
  • 其中set_space_type()函数将新的空页编号设置成$next_space的值。也就是说,只要在GC里,这个页就会当做To空间。set_allocate_type()给函数的页设置了OBJECT标识。
  • while循环用于分配页数大于等于2的时候有效。next_space()函数用来返回被用作参数的页的下一个页。

GC执行过程

下图标识GC执行前堆的状态。这时$current_space和$next_space的值是相同的。首先对$next_space进行增量。一旦GC开始执行,与$current_space值相同的页就是From页,与$next_space值相同的页就是To页。

之后我们将那些保留有从根引用的对象的页“晋升promotion”到To页。下图示:这里的晋升是指将页的编号设定为$next_space的值把它当做To空间处理。

因为对象A是根引用的,所以我们将该对象的页面编号设定为$next_space的值。也就是$next_space = 2

把所有从根引用的页都晋升后,下面就是把To页里的对象的子对象复制到空页了。这个时候对象Y(垃圾对象)引用的D也会被复制过去。然后空页的编号会被设定为$next_space。也就是说这个页变为了To页。

接下来,我们要把追加的To页里的对象的子对象复制到To页的分块里。如果To页里没有分块,那么对象就会被复制到空页,目标页的编号会被设定为$next_space。上图中To页有分块,所以直接复制对象E。如下图示:

当所有对象的子对象复制完毕后GC就结束了,此时$current_space的值设定为$next_space的值。如下图示:

从上图得知,垃圾对象X,Y,D都没有被回收。MostlyCopyingGC的特殊之处就是不会回收包含有从根指向的对象(A)所在页的垃圾对象,并且也不会回收这个垃圾对象所引用的对象群。极端一点,如果所有页里都有对象被根指着,代表所有垃圾不能被回收。

缺页可以通过调整也大小来改善。实验表明页大小适合在512字节。实际上自己在生产环境中那个好就是那个了。

mostly_copying()函数

该方法是用来执行GC的函数,由add_pages()调用。

mostly_copying(){
    $free_size = 0 //为了不把对象复制到From空间里去,GC将From页里的分块大小设置为0
    $allocated_page_num = 0
    
    $next_space = ($current_space) %N // 将next_space进行增量。为了避免$next_space溢出,增量时必取常量N余数。
    
    for(r :$roots) //保留根直接引用的对象所在页。
        promote_page(obj_to_page(*r)) //obj_to_page函数将对象作为参数,返回保留的对的页。
    
    while(is_empty($to_space_queue) == FALSE) //复制To页里的子对象。除去CONTINUED页,所有的To页都连接到了$to_space_queue。我们将其取出并传递给page_scan().
        page_scan(dequeue($to_space_queue))
    
    $current_space = $next_space
    
}

MostlyCopyingGC不会特意把因GC变空的空页的编号置为0.因此空页的编号可能会很混乱,为此常量N的数值必须必空页的总数大得多,以保证及时给所有空页分配唯一的编号,程序也能识别编号被设为$next_space的页和其他的页。

promote_page()函数

是将用作参数的页晋升的函数。如果用作参数的页 里的对象跨了多个页,那么这些页都会被一起晋升。

promote_page(page){
    if(is_page_to_heap(page) == True && space_type(page) == $current_space && allocate_type(page)== OBJECT)
        promote_continued_page(next_page(page)) //下面有源码
        
        // 将晋级的page连接到$to_space_queue
        set_space_type(page, $next_space)
        $allocated_page_num++
        enqueue(page, $to_space_queue)
        
} 
  • 判断条件
    • 是否在堆内
    • 页编号是否和$current_space相同
    • 是否有OBJECT标识
promote_continued_page(page){
    while(space_type(page) == $current_space && allocate_type(page) == CONTINUED) //调查用作参数的页编号是否为$current_space,以及是否设置了 CONTINUED 标志。
        set_space_type(page, $next_space)
        $allocated_page_num++
        page = next_page(page)
}
  • while中调查用作参数的页编号是否为$current_space,以及是否设置了 CONTINUED 标志。如果为真,则参数的页里的对象夸了多个页,这是全部晋升。

对象不被分配到CONTINUED页,其原因就是这里的最后一行代码。如果分配到了CONTINUED页,那么对象就有可能跨页,此时CONTINUED页的下一个页会很有可能也是CONTINUED。如果重新放置到一个空页的话,它是没有下一页的。这就造成了原本不用也不想复制的对象由于在CONTINUED中所以也被复制了

page_scan()函数

把那些持有从根引用的对象的页全部晋升后,下面就要复制到To页里的对象的子对象。

page_scan()函数,是通过mostly_copying()函数调用的函数。这个函数只接受To页作为参数。

page_scan(to_page){
    for(obj : objects_in_page(to_page))
        for(child : children(obj))
            *child = copy(*child)
}

这个函数被用于将页里所有对象的子对象都交给 copy() 函数,并把对象内的指针都改 写成目标空间的地址。

copy()函数

将复制对象用作参数。

copy(obj){
    if(space_type(obj_to_page(obj)) == $next_space) //检查持有obj的页是否是To页。如果是就不会被复制直接返回对象。
        return obj
    
    if(obj.field1 != COPIED)  //检查对象是否复制完毕。
        to = new_obj(obj.size) // 没有复制完毕,则使用该方法来分配空间
        copy_data(to, obj, obj.size) //将对象复制。
        obj.field1 = COPIED  // 修改复制标记,表示已复制。
        obj.field2 = to // 更改指针地址
    return obj.field2 //返回对象地址(也就是目标空间的地址即原对象forwarding)
}

优缺点

优点:使用了GC复制算法,包含它的优点。

缺点:部分垃圾没有被回收。

黑名单

Hans J.Boehm 黑名单法

保守式GC的缺点之一,就是使用指针识别错误,本来要被删除的垃圾却被保留了下来,甚至造成其他更严重的错误。改善这个问题可采用Hans J.Boehm 发明的黑名单法。

指针的错误识别带来的害处

在指针的错误识别中,被错误判断为活动对象的那些垃圾对象的大小及内容至关重要。

  • 大小:有个巨大的对象死掉了,而保守式 GC 却把它错误识别成“它 还活着”,这样当然就会压迫到堆了。
  • 数量:。保守式 GC 会错误识别子对象的子对象,以及子对象的子对象的子对象,错误就会像多米诺骨牌一样连续下去。

黑名单

这个黑名单里记录 的是“不明确的根内的非指针,其指向的是有可能被分配对象的地址”。我们将这项记录操作称为“记入黑名单”。 可能被分配的对象的地址指的是堆内未使用的对象的地址

mutator无法引用至今未使用过的对象如果,根里存在有这种地址的指针,那它肯定就是“非指针”,就会被记入黑名单中。
们在GC标记-清除算法中的 mark() 函数里导入记入黑名单的操作,其伪代码如下。

mark(obj){
    if($heap_start <= obj && obj <= $heap_end)
        if(!is_used_object(obj))
            obj.next = $blacklist
            $blacklist = obj
        else
            if(obj.mark == FALSE)
                obj.mark == TRUE
                for(child :children(obj))
                    mark(*child)
}

如果对象正在使用,is_used_obj()就会返回真。在GC开始时候黑名单会被丢弃,也就是说,在标记阶段需要注意的地址会被记录在新的黑名单里

面向黑名单内存地址分配注意

黑名单里记录的是“需要注意的地址”也就是说这个对象就很可能被非指针值所引用。在将对象分配到需要注意的地址时,为所分配的对象设如下限制条件。

  • 没有小对象
  • 没有子对象少,或者说子对象的孩子加起来不多。

优缺点

优点:保守式 GC 因错误识别指针而压迫堆的问题得到缓解,堆使用效率提升,没有多余对象GC速度也会提升。

缺点:花费时间检测黑名单。

posted on 2018-11-29 16:47  Léon_The_Pro  阅读(287)  评论(0编辑  收藏  举报

导航