垃圾内存回收算法

来源:互联网 发布:医疗大数据概念股 编辑:程序博客网 时间:2024/06/06 09:43

1.引用计数(Reference Counting GC)

引用计数的原理是记录每个对象被引用的次数。new 一个对象当前引用计数为1,将这个对象赋值为另外一个变量,此时的引用计数为2,如果在依次将这个两个变量都置为null,则引用计数为0,回收内存。

Object o1 = new Object(); // 引用计数为1Object o2 = o1; // 引用计数为2o2 = null; // 引用计数为1o1 = 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

0 0