垃圾[1]回收是一种自动管理内存的方式。通常认为手动内管理[2]与垃圾回收相反。
就像其他内存管理技术,垃圾回收可能占据大部分程序处理时间,因而对性能有很大影响。
垃圾回收通常不处理内存之外的资源,比如网络 sockets、数据库 handles、用户交互界面和文件与设备描述符。常用的管理上述资源的方法,尤其是自毁器(destructor)[3],应该足以管理内存,所以不需要垃圾回收(来管理它们)。
优点
垃圾回收将程序员从手动回收内存中解放出来。所以对应种类的 bugs 被消除或者大量减少:
- 野指针[4] bugs,当一片内存被释放而仍然有指针指向它,并且其中一个指针 deference。这时这片内存可能已被重新分配作其它用途,所以可能造成无法预料的结果
- 两次释放 bugs,程序尝试释放已经被释放的内存区域,而这片内存可能已被重新分配作其它用途,所以可能造成无法预料的结果
- 内存泄露,程序不能释放无法接触的对象的内存,所以内存占用只会增长而不会减少
缺点
- 消耗额外的资源
- 影响性能,可能导致程序停顿
垃圾回收消耗计算资源来决定应该释放那些内存,即使程序员已经知道了这个信息。不需要在源码中手动标注对象生存周期的处罚是 overhead[5]。内存层次结构效应[6]会使这个 overhead 在难以预测或常规测试中发现的环境中无法容忍。
垃圾回收的实际执行时刻是难以预测的,会导致停顿(停顿来转换/释放内存)稀疏地分布在一个任务中。难以预测的停顿在实时环境、事务处理或者交互环境中是无法容忍的。
策略
追踪
追踪垃圾回收是最常见的策略。通过一连串从特定根节点对象的引用来追踪哪些对象是可访问的,并把剩余的(不可访问的)节点视为垃圾并回收它们。
引用计数
每个对象有一个引用它的对象数目。引用计数为 0 的对象被视作垃圾。当一个引用它的对象创建时,引用计数加一,当一个引用它的对象销毁时,引用计数减一。
引用计数保证当引用一个对象(设为 A)的最后一个对象被销毁时,该对象(A)会被销毁,并且通常该内存会位于 CPU 缓存、要被释放的对象或者直接被指向的那些对象中,因此对 CPU 缓存和虚拟内存操作不会有很大的副作用。
引用计数有很多缺点,但这通常可以通过复杂的算法来解决或者减轻:
循环引用
当多个对象互相引用,它们会创建一个循环,因此它们的引用计数不会变为 0。一些垃圾回收系统使用特定的循环检测算法来处理这个问题,比如 CPython。
另一个策略是创建弱引用。在引用计数中,弱引用不会增加引用对象的引用计数。当一个引用对象被回收时,所有通过弱引用引用其的对象都会被回收。
空间 overhead
在引用计数中,需要为每个对象分配空间来储存其引用计数。通常使用无符号指针来做这个事,这意味着要为每个对象分配 32 或 64 位的内存空间。
速度 overhead
引用的赋值与回收通常都需要修改一个或多个引用计数器。
需要原子性
在多线程环境中,这些修改(增加和减少)可能需要原子操作,至少针对共享的或可能在多线程中共享的对象要这么做。原子操作的代价是很大的,如果使用软件算法模拟,那么代价会更大。
可以用单线程或单进程来处理引用计数,并且当 local 引用计数变为或不再为 0 时只访问全局引用计数,但这会大大增加内存的负担,因此只在特殊情况下有用(比如 Linux 内核模块)。
非实时
Because any pointer assignment can potentially cause a number of objects bounded only by total allocated memory size to be recursively freed while the thread is unable to perform other work.
参考
- 垃圾:未来不会在系统中或(在其上)运行的程序中的内存中的对象、数据或者其他区域。
- 手动内存管理:程序员手动使用命令来识别和回收垃圾
- 当对象被销毁时自动执行的方法
- 没有指向有效对应或者合适类型的指针
- 完成一个特定任务所需要的计算时间、内存、宽带或其他资源的过度使用或者路线迂回
- 计算机储存基于响应时间被分割为多个层次