首发于java虚拟机
GC算法之一  标记-清除算法

GC算法之一 标记-清除算法

什么是GC标记-清除算法

标记清除顾名思义是一种分两阶段对对象进行垃圾回收的算法。

第一阶段:标记。从根结点出发遍历对象,对访问过的对象打上标记,表示该对象可达。

第二阶段:清除。对那些没有标记的对象进行回收,这样使得不能利用的空间能够重新被利用。

如果用伪代码表示的话则大致如下:

mark_swwep(){
  mark_phase()
  sweep_phase()
}


标记实现:

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

mark(obj){
  if(obj.mark == FALSE)
    obj.mark = TRUE
    for(child : children(obj))
      mark(*child)
}

通过深度优先遍历每个根结点,然后打上标记,就知道哪些对象是存活的。

标记所花费的时间是与存活的数量成正比,时间复杂度为O(N),N为存活对象的数量。


清除的一种简单实现:

sweep_phase(){
   sweeping = $heap_start
   while(sweeping < $head_end)
     if(sweeping.mark == TRUE)
        sweeping.mark = FALSE
     else
        sweeping.next = $free_list
        $free_list = sweeping
     sweeping += sweeping.size 
}

在清除阶段程序会遍历整个堆,对有标记的对象把标记清除掉等待下次GC,对于没有标记的对象则会放到一个单向的空闲列表free_list里面,这样当新建对象需要分配内存时我们就可以从free_list里面取出合适的分块。


对象的内存分配

对于上面提到的标记清除算法,新建对象分配内存时假设需要大小为size,则需要对空闲列表free_list进行一次单向遍历找出大于等于size的块。对于如何找到合适的块有以下三种分配策略:

1、First-fit: 找到大于等于size的块立即返回

2、Best-fit:遍历整个空闲列表,返回大于等于size的最小分块

3、Worst-fit:遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分size大小,并将该部分返回。

这三种策略里面Worst-fit的空间利用率看起来是最合理,但实际上切分之后会造成很多的小块,形成内存碎片,所以不推荐使用。对于First-fit和Best-fit来说,考虑到分配的速度和效率First-fit是更为明智的选择。


优点与缺点:

上面的算法优点:

1、实现简单

2、不移动对象,与保守式GC算法兼容。在保守式GC算法中对象是不能移动的。

算法的缺点:

1、内存碎片化。因为对象不移动,所以导致块是不连续的,容易出现空闲内存很多,但分配大对象时找不到合适的块。

2、分配速度慢。即使是First-fit,但其操作仍是一个O(n)的操作,最坏情况是每次都要遍历到最后。同时因为碎片化,大对象的分配效率会更慢。


优化与改进

多空闲链表

其实产生内存碎片和分配速度慢的主要原因是因为我们的只用到一条空闲链表的缘故。如果我们对块的大小进行分类:1字节的块放在 free_list_1, 2字节的块在free_list_2,..... 直到free_list_100,对于100或100字节以上的块我们统一放到free_list_100里面。这样的话分配对象内存时就不用遍历整个链表而是根据大小到对应的具体的空闲链表,这样的时间会更快。并且因为作为大小区分,很多对象都能找到合适的块,有效的减少和避免了内存碎片的产生。这样的话内存碎片和分配速度慢的问题都可以得有效解决和缓解,perfect!

另外在GC算法中除了标记-清除外,还有复制-清除,标记-整理等算法。这些算法都是相互借鉴,并且有各自的优点。其中标记-整理更是整合了前两种算法,这些我们在后面的文章中会逐一介绍。

编辑于 2018-11-29

文章被以下专栏收录