以下为《垃圾回收的算法与实现》中序章及相关概念读书笔记
1.GC 定义
GC: Garbage Collection, “垃圾回收”
垃圾: GC把程序中不用的内存空间视为垃圾。
1.1 GC所做的两件事:
- 找到内存空间里的垃圾
- 回收垃圾,让程序能再次利用这部分空间
满足以上两项功能的程序就是GC
。
1.2 没有GC会导致哪些问题?
- 内存泄漏:内存空间使用完毕后未释放。手动进行内存管理时,如果忘记释放内存空间,会发生内存泄漏,最终导致内存被占满。
- 垂悬指针(dangling pointer):指向曾经的对象,但是该对象已经不再存在。
int *p = NULL;
void fun() {
int i = 10;
p = &I;
}
int main() {
fun();
printf("\n*p = %d", *p); // *p = 10
printf("\n*p = %d", *p); // 临时变量i已经销毁,*p = 1774530512
return 0;
}
- 错误释放了使用中的内存空间。
1.3 GC 历史:
- GC标记-清除算法(mark-sweep):
John McCarthy身为Lisp之父和人工智能之父,是一名非常有名的黑客,事实上他同时也是GC之父。1960年,McCarthy在其论文 中首次发布了GC算法。
- 引用计数法:
1960年,George E. Collins在论文中发布了叫作引用计数的GC算法。当时Collins可能没有注意到,引用计数法有个缺点,就是它不能回收“循环引用”。Harold McBeth 在1963年指出了这个缺点。
- GC复制算法:
1963年,也有“人工智能之父”之称的Marvin L. Minsky在论文[7]中发布了复制算法。
目前为止发布的所有GC算法都只是以上三种算法的组合。
2.算法相关概念
2.1 对象/头/域
对象:GC世界中,对象表示“通过应用程序利用的数据集合”。(与面向对象编程中表示“具有属性和行为的事物”概念不同)。
对象配置在内存空间里。GC根据情况将配置好对象进行移动或销毁操作。因此对象是GC的基本单位。对象由头(head)和域(field)构成。
2.1.2 头
对象中保存对象本身信息的部分称为“头”。主要包含以下信息:
- 对象的大小
- 对象的种类
通过头来判别内存中存储的对象的边界。
2.1.3 域
对象使用者在对象中可访问的部分。类似于C中结构体的成员。对象使用者会引用或替换对象的域值。另一方面,对象使用者基本无法直接更改头的信息。
域中数据类型分为以下两种:
- 指针
- 非指针 (值本身:数值、字符及布尔)
对象内部,头之后存在1个及1个以上的域。
2.2 指针
GC 根据对象的指针去搜寻其他对象。GC对非指针不进行任何操。
注意点:
1.语言处理程序是否能判别指针和非指针。
2.指针指向对象的哪个部分。大多数语言处理程序中,指针都默认指向对象首地址。如果指针指向对象首地址以外的部分,GC就会变得非常复杂。
2.3 mutator
mutator
"改变某物"的意思,主要是改变GC对象间的引用关系。其实体是“应用程序”,GC在mutator内部进行工作。
mutator实际进行的操作:
- 生成对象
- 更新指针
2.4 堆 Heap
动态(执行程序时)存放对象的内存空间。当mutator申请存放对象时,所需内存空间从这个堆中被分配给mutator。
GC是管理堆中已分配对象的机制。在开始执行mutator前,GC要分配用于堆的内存空间。一旦开始执行mutator,程序就会按照mutator的要求在堆中存放对象。等到堆被对象占满后,GC就会启动,从而分配可用空间。如果不能分配足够的可用空间,一般情况下我们就要扩大堆。
在《垃圾回收的算法与实现》一书“算法篇”中把堆的大小固定为常量
HEAP_SIZE
,不会进行扩大。此外,把$heap_start
定为指向堆首地址的指针,把$heap_end
定为指向堆末尾下一个地址的指针。也就是说,$heap_end
等于$heap_start + HEAP_SIZE
。
2.5 活动对象
活动对象:分配到内存空间中能通过mutator引用的对象。
非活动对象:分配到内存空间中不能通过mutator引用的对象 (需要回收的垃圾)。
2.6 分配(allocation)
分配(allocation):在内存空间中分配对象。当mutator需要新对象时,向分配器申请一个大型合适的空间。
分配器(allocator):在堆的可用空间中寻找满足要求的空间,返回给mutator。
当堆被所有活动对象占满时,再也无法分配可用空间,此时有两种选择:
- 销毁至今为止的所有计算结果,输出错误信息。
- 扩大堆,分配可用空间。
2.6 分块(chunk)
分块在GC中指为利用对象而事先准备出来的空间。
初始状态下,堆被一个大的分块占据。然后根据mutator要求分割成合适大小的块,作为(活动)对象使用。活动对象转为垃圾被回收后,这部分被回收的内存空间再次成为分块,为下次被利用做准备。即内存中的各区块都重复着分块 --> 活动对象 --> 垃圾(非活动对象) --> 分块 ... 的过程
2.7 根 Root
GC中,根是指向对象指针的“起点”部分。能通过mutator直接引用的空间。
$obj= Object.new
$obj.field1 = Object.new
说明: 在这里$obj
是全局变量。首先,我们在第1行分配一个对象(对象A),然后把$obj
代入指向这个对象的指针。在第2行我们也分配一个对象(对象B),然后把$obj.field1
代入指向这个对象的指针。在执行完第2行后,全局变量空间及堆如图所示。在这里我们可以使用$obj
直接从伪代码中引用对象A,也就是说A是活动对象。此外,因为可以通过$obj
经由对象A引用对象B,所以对象B也是活动对象。因此GC必须保护这些对象。
GC把上述这样可以直接或间接从全局变量空间中引用的对象视为活动对象。
与全局变量空间相同,我们也可以通过mutator直接引用调用栈(call stack)和寄存器。也就是说,调用栈、寄存器以及全局变量空间都是根。
3.GC评价标准
3.1 评价GC算法性能的四个标准
- 吞吐量
- 最大暂停时间
- 堆使用效率
- 访问的局部性
3.1.1 吞吐量
单位时间内处理能力
如图,mutator执行过程中,GC启动3次,耗时分别为分别为A、B、C。则总耗时(A+B+C)。 假设堆大小为HEAP_SIZE
,则吞吐量为HEAP_SIZE/ (A+B+C)
。
吞吐量也受到mutator动作影响。
当然,人们通常都喜欢吞吐量高的GC算法。然而判断各算法吞吐量的好坏时不能一概而论。打个比方,众所周知GC复制算法和GC标记-清除算法相比,活动对象越少吞吐量越高。这是因为GC复制算法只检查活动对象,而GC标记-清除算法则会检查所有的活动和非活动对象。然而,随着活动对象的增多,各GC算法表现出的吞吐量也会相应地变化。极端情况下,甚至会出现GC标记-清除算法比GC复制算法表现的吞吐量更高的情况。
3.1.2 最大暂停时间
指“因为执行GC而暂停执行mutator的最长时间”。(图1.7中最大暂停时间是B)
3.1.3 堆使用效率
影响因素:
头大小:
在堆中堆放的信息越多,GC的效率也就越高,吞吐量也就随之得到改善。但毋庸置疑,头越小越好。因此为了执行GC,需要把在头中堆放的信息控制在最小限度。堆的用法
根据堆的用法,堆使用效率也会出现巨大的差异。举个例子,GC复制算法中将堆二等分,每次只使用一半,交替进行,因此总是只能利用堆的一半。相对而言,GC标记-清除算法和引用计数法就能利用整个堆。
堆使用效率和吞吐量以及最大暂停时间不可兼得。即:可用堆越大,GC运行越快;相反,越想有效利用有限的堆,GC花费时间越长。
3.1.3 访问的局部性
PC上四种存储器:寄存器、缓存、内存、辅助存储器(磁盘)。存取速度一般与存储容量成反比。
一般我们会把所有的数据都放在内存里,当CPU访问数据时,仅把要使用的数据从内存读取到缓存。与此同时,我们还将它附近的所有数据都读取到缓存中,从而压缩读取数据所需要的时间。
另一方面,具有引用关系的对象之间通常很可能存在连续访问的情况。这在多数程序中都很常见,称为“访问的局部性”。考虑到访问的局部性,把具有引用关系的对象安排在堆中较近的位 置,就能提高在缓存中读取到想利用的数据的概率,令mutator高速运行。