1.引用计数(Reference Counting GC)
引用计数的原理是记录每个对象被引用的次数。new 一个对象当前引用计数为1,将这个对象赋值为另外一个变量,此时的引用计数为2,如果在依次将这个两个变量都置为null,则引用计数为0,回收内存。
Object o1 = new Object(); // 引用计数为1
Object o2 = o1; // 引用计数为2
o2 = null; // 引用计数为1
o1 = null; // 引用计数为0,回收内存
缺点
不能处理循环引用问题
2.标注并清理(Mark and Sweep GC)
程序一直运行,直到内存空间不足,触发GC操作,标志并清理的原理是获取所以得“GC Roots”深度遍历内存的引用,能遍历到的增加一个标志,最好清理没有标志的内存块,并将标志还原。
标注并清理伪代码
void gc() {
suspendAllThreads();
// 遍历标志
List<Object> roots = GetRoots();
for(Object root : roots ) {
mark(root);
}
// 清理
sweep();
resumeAllThreads();
}
标志伪代码
void mark(Object* pObj) {
if ( !pObj->IsMarked() ) {
// 修改对象头的Marked标志
pObj->Mark();
// 深度优先遍历对象引用到的所有对象
List<Object *> fields = pObj->GetFields();
for ( Object* field : fields ) {
mark(field); // 递归处理引用到的对象
}
}
}
清理伪代码
void sweep() {
Object *pIter = GetHeapBegin();
while ( pIter < GetHeapEnd() ) {
if ( !pIter->IsMarked() ) {
Free(pIter);
} else {
pIter->UnMark();
}
pIter = MoveNext(pIter);
}
}
当前引用关系
标注出已经被引用
清理后
优点
处理了循环问题
如果内存够大,对程序没有额外的开销
缺点
GC是需要挂载其它的组件
3.标注并整理(Mark and Compact GC)
这种方法是标注并清理的一个变种,在标注并清理方法中反复申请和释放内存可能出现大量的内存碎片,在标注并整理方法中会在清理过程中会移动移动存活的对象,使其紧凑的排列,如下图:
处理前
处理后
4.拷贝回収法(Copying GC)
这也是标注并清理法的一个变种,内存分为两个部分A和B的部分,先使用A部分,当A使用完,通过标注法将活动的对象紧凑的拷贝到B,此时使用B,当B使用完,标注拷贝到A,如此循环进行。
处理前
处理后
5.逐代回收法(Generational GC)
也是标注法的一个变种,标注法最大的问题就是中断的时间过长,此算法是对标注法的优化基于下面几个发现:
- 大部分对象创建完很快就没用了 – 即变成垃圾;
- 每次GC收集的90%的对象都是上次GC后创建的;
- 如果对象可以活过一个GC周期,那么它在后续几次GC中变成垃圾的几率很小,因此每次在GC过程中反复标注和处理它是浪费时间。
可以将逐代回收法看成拷贝GC算法的一个扩展,一开始所有的对象都是分配在”年轻一代对象池” 中 – 在JVM中其被称为Young
第一次垃圾回收过后,垃圾回收算法一般采用标注并清理算法,存活的对象会移动到”老一代对象池”中– 在JVM中其被称为Tenured,如图 14 - 12,而后面新创建的对象仍然在”年轻一代对象池”中创建,这样进程不停地重复前面两个步骤。等到”老一代对象池”也快要被填满时,虚拟机此时再在”老一代对象池”中执行垃圾回收过程释放内存。在逐代GC算法中,由于”年轻一代对象池”中的回收过程很快 – 只有很少的对象会存活,而执行时间较长的”老一代对象池”中的垃圾回收过程执行不频繁,实现了很好的平衡,因此大部分虚拟机,如JVM、.NET的CLR都采用这种算法。
在逐代GC中,有一个较棘手的问题需要处理 – 即如何处理老一代对象引用新一代对象的问题,如图 14 - 13中。由于每次GC都是在单独的对象池中执行的,当GC Root之一R3被释放后,在”年轻一代对象池”中执行GC过程时,R3所引用的对象f、g、h、i和j都会被当做垃圾回收掉,这样就导致”老一代对象池”中的对象c有一个无效引用。
为了避免这种情况,在”年轻一代对象池”中执行GC过程时,也需要将对象C当做GC Root之一。一个名为”Card Table”的数据结构就是专门设计用来处理这种情况的,”Card Table”是一个位数组,每一个位都表示”老一代对象池”内存中一块4KB的区域 – 之所以取4KB,是因为大部分计算机系统中,内存页大小就是4KB。当用户代码执行一个引用赋值(reference assignment)时,虚拟机(通常是JIT组件)不会直接修改内存,而是先将被赋值的内存地址与”老一代对象池”的地址空间做一次比较,如果要修改的内存地址是”老一代对象池”中的地址,虚拟机会修改”Card Table”对应的位为 1,表示其对应的内存页已经修改过 - 不干净(dirty)了
当需要在 “年轻一代对象池”中执行GC时, GC线程先查看”Card Table”中的位,找到不干净的内存页,将该内存页中的所有对象都加入GC Root。虽然初看起来,有点浪费, 但是据统计,通常从老一代的对象引用新一代对象的几率不超过1%,因此”Card Table”的算法是一小部分的时间损失换取空间。
JVM采用逐代回收法
在Android中 ,实现了标注与清理(Mark and Sweep)和拷贝GC