「垃圾回收的算法和实现」 第七章-分代垃圾回收

Posted by Dawn-K's Blog on January 27, 2022

分代垃圾回收

背景

分代垃圾回收基于这样一个事实:

  1. 很多对象出生不久就会死亡
  2. 已经存在了很久的对象在未来一段时间内大概率还是存活

分代垃圾回收并没有创造新的算法,而是根据代的不同而采用不同的算法。对象刚创建是新生代,然后存活一段时间后就 晋升 老年代。

JVM 中就是这个算法。

Ungar 的分代垃圾回收

堆空间总的分为两个,一个是新生代空间,一个是老年代空间。其中新生代空间有分为生成空间和幸存者空间 from 和幸存者空间 to.

除了堆之外,我们还需要一个数组,称之为记录集,它用来记录老年代对新生代的引用。

在新生代空间中,对象产生于生成空间,在生成空间满的时候进行 gc,生成空间和 from 都向 to 进行 gc 复制算法。然后切换 from 和 to 的名字。记录集的作用就是用来记录老年代对象对新对象的引用。

当新生代通过晋升将老年代塞满的时候执行老年代 gc,是标记清扫法。

记录集

记录集往往设置为指向对新生代对象有引用的老年代对象。这是为了能够修复引用。

书中提到了一个 写屏障 的概念,这个我存在一些疑惑,和搜到的不一样。

一个对象头中有三个信息

  1. age 只对新生代对象有效
  2. remembered , 老年代对象只看这个
  3. forwarded 只对新生代对象有效,是一个布尔值,用于 gc 复制,表示是否复制完成了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 在这里的意思基本和之前 UpdatePtr 是一个使用时机
// obj 对象的 field 域指向了 newObj 对象
writeBarrier(obj,field,newObj){
    // 如果 obj 是一个没有在记录集的老年代对象
    // 指向的 newObj 是新生代对象
    if(obj >= $oldStart && newObj< $oldStart && obj.remembered == False){
        // 加入记录集
        $rs[$rsIdx++] = obj;
        obj.remembered = True;
    }
    // 真正的指向
    *field = newObj;
}

newObj(size){
    // 生成空间如果不足就 gc,对象只能生成在生成空间(不能生成在幸存空间)
    if($newFree + size >= $surviveStart){
        minorGC();
        if($newFree + size >= $surviveStart){
            allocationFail();
        }
    }
    obj = $newFree;
    obj.size = size;
    obj.age = 0;
    obj.forwarded = False;
    obj.remembered = False;
    $newFree += size;
    return obj;
}

// 这里不去考虑如果生存空间和 from 活动对象超过了 to 空间大小的情况

这里比较复杂,先语言描述一下。

minorGC

首先向 to 空间执行 gc 复制算法,如果对象的 age 还没到上限,那么就正常复制,否则就晋升到老年代。我们不讨论 to 空间不足的情况。

然后遍历新对象的 children 对象,如果子对象不在老年代,就使用前文的 writeBarrier 进行记录集的增添。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
minorGC(){
    for(r : $roots){
        // 只处理新生代的对象
        if(r < $oldStart){
            r = copy(r);
        }
    }

    i = 0;
    // 遍历记录集,从记录集中找到活动对象
    while(i < $rsIndex){
        for(child : children($rs[i])){
            if(child < $oldStart){
                child = copy(child);
                if(child < $oldStart){
                    hasMinorObj = True;
                }
            }
        }

        // (经历过 gc 复制算法后)没有指向新生代的子对象 就从记录集中删掉它
        if(!hasMinorObj){
            $rs[i].emembered = False;
            $rsIndex--;
            swap($rs[i],$rs[$rsIndex]);
        }else{
            ++i;
        }
    }

    swap($fromStart,$toStart);
}

copy(obj){
    // 已经复制完毕的
    if(obj.forwarded){
        return obj.forwarding;
    }

    if(obj.age < AGEMAX){
        // 复制到 to
        newObj = copyData(obj,$toFree);
        $toFree += obj.size;
        obj.forward = newObj;
        obj.forwarded = True;
        newObj.age ++;
        for(chlid : children(child)){
            child = copy(child);
        }
    }else{
        promote(obj);
    }
    return obj.forward; 
}

promote

对象的 forwarded 标签用于表示它有没有已经被赋值。

晋升到老年代的操作 promote :如果老年代空间不足,就进行老年代 gc,再不足就报错。复制到老年代之后,让原来的对象的 forwarded 为 True,forwarding 指向刚才的新对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
promote(obj){
    newObj = allocateInOld(obj);
    // 如果老生代没有空间,就尝试清理
    if(newObj == NULL){
        // 老年代 gc 就是标记清扫算法
        majorGC();
        newObj = allocateInOld(obj);
        if(newObj == NULL){
            allocationFail();
        }
    }

    obj.forward = newObj;
    obj.forwarded = True;
    obj.remembered = False;

    // 检查是否需要加入记录集
    for(chlid : children(child)){
        if(child < $oldStart){
            $rs[$rsIndex] = child;
            #rsIndex++; 
            obj.remembered = True;
            return
        }
    }
}

评价

这个算法的优化完全建立在”刚产生的对象比产生很久的对象更容易变成垃圾“这个经验上。

在一般情况下能够起到比较好的效果,但是并不符合这个规律的效果甚至不如普通 gc 算法。