首发于java虚拟机
GC算法之二 复制-清除算法

GC算法之二 复制-清除算法

GC复制算法(Copying GC)是由Marvin L. Minsky在1963年研究出来的算法。原理是把内存分为两个空间一个是From空间,一个是To空间,对象一开始只在From空间分配,To空间是空闲的。GC时把存活的对象从From空间复制粘贴到To空间,之后把To空间变成新的From空间,原来的From空间变成To空间。回收前后对比下图所示


算法实现

如果手动编码实现复制清除回收算法的话,大概如下:

  void copying(){
        $free = $to_start //$free表示To区占用偏移量,每复制成功一个对象obj,$free向前移动size(obj)
        for(r : $roots)
            *r = copy(*r) //复制成功后返回新的引用

        swap($from_start, $to_start) //GC完成后交互From区与To区的指针
    }

上面代码分为三步:

1、遍历根结点集合roots。

2、复制根结点及其引用的结点到To空间,并返回新引用。

3、复制完成后,把指向From空间和To空间指针相互交换。


复制的过程:

copy(obj) {
        if (obj.tag != COPIED)
            copy_data($free, obj, obj.size)
        obj.tag = COPIED
        obj.forwarding = $free
        $free += obj.size

        for (child:
             children(obj.forwarding))
         *child = copy( * child)

        return obj.forwarding
    }

1、对于任意对象我们都需要记录有没有被复制过,如果有则obj.tag = COPIED。如果不记录标记就会造成重复复制,比如A可以引用B,同样C也引用B,不标记则B会被复制两次。

2、用obj.forwarding记录对象在新空间(To空间)的偏移量(内存地址),还记得在copying方法中我们把$free赋值了$to_start吗。每复制一个对象$free就向前移动obj.size的,这样后面复制对象时就可以保持对象位置的同步更新,同时$free也记录了当前空间使用了多少内存。

3、返回新的对象引用替换到旧的引用, child = copy(*child); return obj.forwarding。如果不替换掉,那么程序中的引用就不能访问到复制到To空间最新对象,还是访问原来在From空间的老对象,老对象被回收后程序还继续访问或者访问到旧值就会触发不可预知的异常!

空间分配

在GC完成后原有的From空间已经变成了To空间,旧To空间变成新的From空间。所以此时对象分配落在新From空间,又因为我们复制对象后把对象紧凑的拼合在了一起,如开头图片所示~。此时内存的分配就相对简单了,只需要把$free指针移动对应大小的位置就可以了。假定From空间和To空间都等于二分之一堆空间(HEAP_SIZE),则分配的过程:

new obj(size){

            //判断此时内存不足,进行GC收集
            if($free+size>$from_start+HEAP_SIZE/2)
            copying()
            //GC后还是不足,则进行分配内存失败处理
            if($free+size>$from_start+HEAP_SIZE/2)
            allocation_fail()

            obj=$free
            obj.size=size
            $free+=size
            return obj
            }

是不是觉得GC复制算法分配内存的过程很简单~

优缺点分析

对于任意的GC算法我们都可以从吞吐量(GC延迟),内存的分配效率,内存碎片化,堆的使用效率这个几个方面评估。

吞吐量(GC延迟):正所谓没有对比就没有伤害。另一种算法GC标记-清除消耗的吞吐量是搜索存活对象(标记阶段)和搜索整堆(清除阶段)所花的时间之和。而GC复制算法只搜索并复制存活的对象,少了访问整堆和构造空闲链表的操作能够在短时间内完成GC。换言之,GC复制的吞吐量要比标记-清除要优秀,并且堆越大这种差距越明显。

内存的分配效率:GC复制算法不使用空闲链表,因为分块本身就是一个连续的空间。只要新建的对象不超过剩余空间的大小,只需要移动$free指针即可~。所以GC复制算法的分配效率非常的高效。

内存碎片化:没有内存碎片化。因为是分块是连续的空间,对象都是按需分配,紧凑的挨在一起。

堆的使用效率:因为To空间一直都是空闲的,如果To的空间很大就会造成明显内存浪费。这可以说是GC复制算法一大缺陷吧。如果To空间定义的太小,复制时放不下存活的对象就会导致程序异常。如果定义足够大,则分给From空间太小,反过来容易增加GC的次数。所以GC复制算法的难点在于定义From空间与To空间的比例。

不过GC总体来说复制算法还是很优良的,JVM对堆里面新生代的垃圾回收就运用了GC复制算法,并根据java对象的存活特点作了相应的改良:把堆分成了一个From区,二个Survior区,比例为8:1:1,保证了GC高效同时提高了堆的使用效率哈。

编辑于 2018-11-29

文章被以下专栏收录