GC系列:如何优化引用计数算法(1)

来源:互联网 发布:java什么类不能被继承 编辑:程序博客网 时间:2024/05/29 12:46

    引言

    标记-清除,标记-整理,复制式回收算法都是属于间接式的:先从根集合出发,遍历根集合图,找到存活的对象,再反向确定出死亡的对象。而引用计数算法则可以通过引用关系的创建和删除直接确定对象的存活状态,而不用像追踪式回收器一样。
    在引用计数算法中,每个对象都维护了一个引用计数器,该值通常在对象头中。

    伪代码

简单的引用计数算法New()://创建对象    ref=allocate()//调用分配方法    if(ref ==null)//如果是null表示没有可用空间,则抛出OOM异常        error 'out of memory'    rc(ref)=0//设置对象的引用计数=0    return ref//返回该对象atomic Write(src,i,ref)://原子操作    addReference(ref)//增加新对象的引用    deleteReference(src[i])//删除旧对象的引用    src[i]=ref//令旧对象的指针指向新对象addReference(ref)://增加引用计数    if(ref !=null)    rc(ref)+=1;deleteReference(ref)://减少引用计数    if(ref !=null)    rc(ref)-=1;    if( rc(ref) )=0//如果引用计数为0则回收该空间    for each fid in Pointers(ref)        deleteReference(*fld)    free(ref)

    从上面这段伪代码中,需要注意2点:
    1.对引用计数的修改需要新增加,再减少,避免当src[i]=ref时,如果先执行-1可能会导致对象的引用计数为0而直接回收。
    2.当一个对象的引用计数为0时,就会立即引发回收操作回收该对象所在的空间,同时减少所有子节点的引用计数。
    3.Write方法是一个原子操作,会存在一定的开销

    优点:

    引用计数算法的内存管理开销分摊在程序运行过程中,一个对象一旦成为垃圾就能立马得到回收,所以引用计数算法可以持续操作即将填满的堆而不必像追踪式回收器一样保留一定的空间。
引用计数算法直接操作指针的来源与目标,不会破坏局部性。
引用计数算法无须确定程序的根,即使当系统部分不可用时,也能回收部门内存。

    缺陷:

    1.给赋值器带来了额外的开销。与追踪式回收器相比较,为了管理引用计数,对read方法和write方法进行了重定义,导致对对象的读操作也会带来一定的开销。
    2.为了避免多线程竞争导致的释放对象过早,引用计数的增减,加载和存储指针的操作也必须是原子的。
    3.无法处理环状数据结构(如双向链表)
    4.最坏情况下,某一个对象的引用计数可能等于整个堆中所有对象的数量,意味着引用计数所占的域必须占一个完整的槽,这一空间开销较大。
    5.可能会导致停顿的出现:删除大型指针结构根节点的最后一个引用时,会递归得删除根节点的每一个子孙节点,可能会导致最大停顿时间比追踪式的还长。(可以引入懒惰引用计数策略,但也会引入新的问题)

    怎样提升引用计数算法的效率

    1.延迟引用计数。
    2.合并引用计数:在单个时间段内,只关注对象是否第一次被修改,针对同一对象的再次修改则会被忽略。
    3.缓冲引用计数:将所有的引用计数操作缓冲起来以便后续处理,同时只有回收线程可以执行引用计数变更操作。

    延迟引用计数:

    只有当赋值器操作堆中的对象时产生的引用计数变更才会立即生效,而操作栈或者寄存器(局部变量)所产生的变更则会延迟执行—>代价:引用计数器不再准确,所以不能立即回收引用计数等于0的对象,转而要引入stop the world来定期修正引用计数。
    当引用变成0之后,需要将其添加到零引用表中(零引用表中的对象都是引用计数为0但可能仍然存活的对象,当赋值器把零引用对象的引用写入到堆中某一对象时,可以将其从零引用表中移除)。
    当堆中内存耗尽时就必须进行垃圾回收:挂起所有赋值器线程并检查零引用表。怎么确定零引用表中的对象是否存活呢?最简单的办法是对根指向的对象进行扫描并增加引用计数。完成这一步后,所有被根引用的对象的引用计数都大于0,那些仍然为0的就是需要回收的垃圾了。

    合并引用计数:

    延迟引用计数解决了赋值器操作局部变量时的引用计数变更。而合并引用计数则只需关注对象在某一个时段开始和结束时的状态,忽略中间的变化。

    具体来讲,就是在某一个时段开始前,在回收器日志中记录下A都引用了哪些对象(如图是B,C),然后在该时段结束时,得到A现在都引用了哪些对象(B,D),然后先对回收器日志中记录的对象的引用计数都-1,再对现在引用的对象的引用计数+1.(B先-1,再+1,最后引用计数保持不变。C减1。D直接+1)

    将延迟引用和合并引用结合在一起可以显著大部分赋值器上的开销,但是我们也付出了一定的代价,即引入了停顿,降低了回收的时效性,日志缓冲区和零引用表也带来了额外的空间开销。需要注意的是,合并引用计数中,对于一个未修改的指针,它所指向的对象仍可能需要回收器增加和删除引用计数1次(如图B)

原创粉丝点击