名词解释
- 垃圾:无法再被访问的对象或内存空间
- 延迟:平均每次垃圾回收开始到结束需要的时间。
- 吞吐量:平均一定时间内能回收多少内存,内存多少这个概念非常广泛,可以指多少个对象,也可以指多少字节的空间,具体的应该看指标应需求而异。
- 根节点:如全局变量上的对对象的引用、栈上对对象的引用等用户一定能够访问到的地址,是寻找活对象的入口。
垃圾回收算法
- 引用计数
- 标记清除
- 标记复制
- 标记整理
引用计数
这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。我们可以为每个对象都增加一个计数器,来记录对这个对象的引用数量,当引用计数归零时,这个对象变成了垃圾
核心思想
- 设置引用数,判断当前引用数是否为0
- 需要一个引用计数器
- 引用关系发生改变时改变引用数字
- 引用数字为0是立即回收
优点
- 内存释放及时,当一个对象死亡时其占用的内存马上被释放
- 延迟低,内存释放的时间均匀地分布在各个时间段
缺点
- 每个对象需要附带一个计数字段的空间
- 引用复制和销毁时需要对改变计数字段,这可能涉及到相对昂贵的原子操作
- 无法处理循环引用,比如两个对象互相引用对方的情况
使用举例
- cocos2dx底层框架
进阶话题
当一个大对象的引用归零时,常常会导致一大批的对象引用归零,这种成批释放的情况非常常见,会导致垃圾回收的延迟上升以及可能占用大量栈空间去递归释放,循环引用可以通过一些算法检测到,也可以在适当时刻使用其他垃圾回收算法来释放引用计数器的更新是存在冗余的,即一大部分的引用计数的更新是可以被消除的
标记清除 Mark-Sweep
它分为标记和清除两个阶段。当垃圾回收被触发时,运行时从有限的根节点(在Javascript里,根是全局对象)出发,对所有能够到达的对象进行标记(一般为深度优先搜索),然后再遍历整个堆,清除所有未被标记的对象。
优点
- 相比引用计数很难处理循环引用,Mark-Sweep算法总能找到所有无法被引用的对象
- 由于垃圾总被一起批量回收,可能可以提高内存回收的吞吐
- 这个算法实现起来简单
缺点
- 每个对象需要附带至少一个比特作为标记的空间
- 由于Mark阶段需要在整个堆上随机遍历,对CPU缓存不友好
- 算法的性能与堆的大小相关,当堆非常大,而单次回收对象数量有限时,性能被严重拖累
- 垃圾回收的延迟较高,会使用户代码完全停止一段时间
- 垃圾收集后有可能会造成大量的内存碎片,出现内存不连续的状态
进阶话题
增量标记,即通过对算法一定的修改,Mark阶段可以与用户程序交替执行直到标记阶段完成,以减少垃圾回收算法的延迟。例如:lua的三色标记算法
标记复制 Mark-Copy
Mark-Copy将堆内存一分为二,一个处于使用状态,一个处于闲置状态。当开始垃圾回收时,会检查使用状态的内存块,把存活的对象复制到闲置状态的内存块,完成复制后,两个内存空间交换角色。
优点
- 在回收垃圾的同时也整理内存,避免了内存碎片化的问题
- 非侵入式的算法,不需要对象上的字段(理想是美好的,但现实往往不是)
- 算法的执行时间仅与活对象的数量有关,不需要扫描整个堆
- 分配对象时不需要寻找空闲空间,因为其总在当前使用的堆的末尾
缺点
- 回收时需要进行大量的内存拷贝
- 内存利用率低,维护了两个堆,却只用了一半的空间
进阶话题
- 通过分块的方式维护N个堆,以提高内存利用率
- 对活对象进行分代维护
标记整理 Mark-Compact
注意Mark-Copy算法需要维护一个额外的堆来作为拷贝活对象的容器。标记整理和标记清除的差别在于对象标记死亡后,在整理内存的过程中,将活着的对象往一端移动,移动完成后,直接清理边界外的内存。
可以说Mark-Compact是Mark-Copy和Mark-Sweep算法的一种整合,其优缺点也只是前两种算法各取部分。
特点比较
(1)标记清除只清除死亡的对象,速度较快
(2)标记复制只复制活着的对象,用空间换取时间
(3)标记整理是两者的整合,速度最慢
引擎扩展举例
cocos2dx 引用计数
详情请看底层源码
class CC_DLL Ref
{
public:
void retain();
void release();
Ref* autorelease();
unsigned int getReferenceCount() const;
protected:
unsigned int _referenceCount;
}
主要流程:
- 对象基类有一个引用计数器 _referenceCount来标记对象被引用的次数
- retain()使_referenceCount+1,
- release使_referenceCount-1,
- 当_referenceCount为0时,回收对象
lua 三色标记算法
在前面的双色标记法中,我们可以看到一个对象可以分为白色和黑色。现在引入一个灰色的概念,标记那些已经被扫描到但是所引用的对象没有被扫描完的对象。
颜色标记:
- 白色:尚未访问过。
- 黑色:本对象已访问过,而且本对象 引用到 的其他对象 也全部访问过了。
- 灰色:本对象已访问过,但是本对象 引用到 的其他对象 尚未全部访问完。全部访问后,会转换为黑色。
主要流程:
- 1、开始的时候,会认为所有的对象都是白色的;
- 2、利用根可达算法将根下的引用对象都标记为灰色
- 3、移动到 灰色对象中,将本对象 标记为黑色;
- 4、将灰色对象中所有引用对象标记为灰色;
- 5、重复第3第4步,直至扫完所有灰色对象;
- 6、完成后没有被标记为黑色的,或者全部是白色的,就为mark sweep出来的垃圾对象可以进行回收。
流程特点:
- 第2、3步是可以分步进行的,但是要对新元素进行处理,避免遍历遗漏。
V8分代收集法
这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。这样就可以根据各个年代的特点采用最适合的收集算法
新生代
- 顾名思义,新生代指新建对象存放的地方。一般来说,新建对象存放在新生代中,eg:局部变量。90%以上的对象都是生命周期较短的
- 新生代中分为3部分,1个eden区和2个survivor区(survivor0和survivor1),默认情况下,eden区与survivor区的比例为8:1:1
- 复制算法的过程:程序创建对象发现eden区内存不够时,JVM的垃圾回收器将对eden区进行垃圾回收(Minor GC),将eden区中的不可回收的对象复制到survivor0区。如果survivor0区内存不够,则将survivor0区中不可回收的对象复制到survivor1区,然后将eden区和survivor0区中所有对象回收。
老年代
- 在新生代survivor区中生命周期足够久的对象将会晋升到老年代中,eg:缓存。生命周期足够久的概念:默认情况下经历了15次GC,或者超过-XX:MaxTenuringThreshold参数配置的次数
- 老年代内存空间比新生代大(默认老年代:新生代为1:2),老年代内存满后触发Full GC,使用标记-整理算法进行垃圾回收。Full GC花费的时间远长于Minor GC