常见垃圾回收
垃圾回收介绍
程序运行过程中,函数的局部变量、参数和返回值都在栈中。
在函数返回后,该函数调用栈会被销毁,一些不能在编译阶段就确定大小的对象、或生命周期超出当前所在函数的对象就不适合分配在栈上,需要分配在堆上。
分配在栈上的数据会随着栈的销毁而释放自身占用的内存,但是堆上的数据需要程序主动释放。
手动进行垃圾回收:如,CPP要求程序员自己控制垃圾回收,但是数据释放早会导致之后的访问出错(悬挂指针问题),忘记释放则会数据一直占用内存导致内存泄漏。故多种编程语言支持自动垃圾回收。
自动垃圾回收:程序员不再需要关心内存何时被释放,被释放的内存如何处理,都交给语言自带的垃圾回收功能处理。
常见垃圾回收算法
引用计数式
为每一个对象维护一个引用计数,每当引用该对象的对象被销毁,引用计数就减1,引用计数器为0则回收该对象。
优点:对象及时被回收,不会出现内存耗尽或者达到某个阈值才回收
缺点:不能很好地处理循环引用,实时维护引用计数也需要代价
追踪式回收
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">标记清除(Mark-Sweep)</font>
从栈、数据段上的根变量开始遍历所有引用的对象,能够通过遍历访问到的对象标记为"被引用"。没有被标记的内存进行回收。
三色标记法(类似BFS遍历):
- 最开始所有的对象都是白色
- 从根扫描所有可达的对象,标记为灰色,放入待处理队列
- 从队列取出灰色对象,其引用的对象都标记为灰色继续放入队列,自身则标记为黑色
- 不断重复3操作,直到队列为空。此时所有的白色对象即为垃圾,可以回收。
三色标记法可以让标记过程过程和用户程序并发的进行。

<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">标记-压缩(Mark-Compact)</font>
标记清除比较容易出现内存碎片问题,故可以完成标记工作后,移动非垃圾数据,让其紧凑的放在内存中。
但是其多次扫描和移动开销也很大。

<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">复制回收</font>
将堆内存分为From、To两部分,程序执行时使用From空间,垃圾回收时扫描From空间将能追踪到的数据给复制到To空间,回收全部的From空间,From和To交换。

该方式不会产生内存碎片问题,但是会让堆内存只有一半在使用,故可以只在一部分堆内存中使用该方法。

<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">分代回收</font>
新创建的对象称为新生代对象,多次垃圾回收仍然存活的对象称为老年代对象。
弱分代假说:大部分对象都会在年轻时死亡。也就是说新生代对象称为垃圾的概率高于老年代对象。
将数据分为新生代和老年代,可以降低老年代的回收频率,不用每次处理所有数据;新生代和老年代也可以使用不用的垃圾回收策略。
并发/并行执行垃圾回收
每次进行垃圾回收都需要暂停用户程序,即STW(stop the world)。
但是用户难以接收一次性暂停长段时间进行完整的GC工作,故可以将GC分为多次完成,使得用户程序和GC交替进行,缩短每一次暂停的时间。
或是多核并行执行用户程序和GC,则无需对用户程序进行STW。

但是并发/并行执行又会产生新的问题:
以三色标记法为例,若在GC时将A标记为黑色,即扫描完成,不会再进入待处理队列;下一次的用户程序中又让A引用了一个没有任何别的引用的白色对象B;再进行GC时由于黑色对象A不会再被扫描,其引用的白色对象B就会被当为垃圾被清理,导致A找不到B。
简单来说就是,白色被挂在黑色上的同时灰色丢失了对该白色的引用
解决办法:
- 强三色不变式:不允许黑色对象对白色对象的引用。
- 弱三色不变式:黑色对象对白色对象的引用,但必须保证有灰色对象也引用该白色对象
读写屏障
为了防止并行/并发GC时出现误删除的情况,引入了屏障机制。
屏障机制类似一种额外的判断机制。
写屏障
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">插入写屏障</font>
当一个对象被引用时触发该机制。
在A对象引用B对象时,B对象就被标记为灰色;即B挂在A的下游,B必须被标记为灰色,满足强三色不变式。
但是插入写屏障仅仅在堆上使用,而不在栈上使用。
为了保证栈上被引用的白色对象不会被清除,在回收白色对象之前,会进行STW并对栈空间重新进行一次三色标记。
插入写屏障的不足也在于此,GC的最后需要进行一次短时间的STW来重新标记一遍栈。
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">删除写屏障</font>
当对象被删除引用时触发该机制。
被删除引用的对象,会被标记为灰色;满足弱三色不变式。
但是由于被删除的对象变为了灰色,当次GC不会将其清理,需要等到下一次GC才能清理它。
故,删除写屏障的不足在于,回收精度低,删除的对象不能立即清理。
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">读屏障</font>
标记-清除这类GC不会进行移动数据,天然不需要读屏障。
复制回收这种会移动数据来避免碎片化的GC,当GC和用户程序交替执行,会出现问题,如:
GC将A从From被复制到To;用户程序B引用了A,但是此时B中包含的A指针指向的是From中的A;
GC将B复制进To并清除From,则B中的A指针便找不到引用的A数据了。

此时需要读屏障来保证用户程序不会访问到在From中已经复制到To中的对象。
可以在检测到A已经复制到To中时,就让用户程序去读取To中A复制,而不要读取From中的A。