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 阅读(298) 评论(0) 编辑 收藏 举报