垃圾收集技术 2018 程序啪啪啪
GC
标记清扫算法
标记数据
- 位图
- 额外空间开销
- 减少了标记阶段中的缺页错误和cache写失误的发生率
- 速度较快
- Dalvik 实现中是有两个bitmap,一个用于记录目前的活动对象,一个用于记录标记过程的可触及对象。然后暂停系统,对比bitmap进行释放标记。基于bitmap,实现一个根保守的栈mark操作成为可能。
- 对象
- 具有跳跃性
- 影响cache
- 栈溢出(引用层次太多)
- 显示调用栈(容易栈溢出)
- 手工维护
- 定量栈+遍历堆
- 反向指针
- 循环使用栈
- 具有跳跃性
清扫阶段
- 立刻清扫
- 延迟清扫
- bdw
- Boehm-Demers-Weiser(多层次分配器,高层次分配器将请求流转到不同的子分配器,子分配器只负责统一大小数据的分配和回收。)
- 子分配器
- 采用渐进式回收
- 考虑数据组织结构
- 子分配器
- Hughes(每次分配时完成一定数量的清扫工作。对alloc的每次调用都会清扫堆,直到它找到一个合适的自由节点。)
- 回收中断变短
- 来回操作位图,不高效
- 能够直接利用回收节点,不用交还给空闲列表
- Zorn(也是分层次的数据组织形式,不过不是用链表串联,而是是用一个数组进行管理。)
- 遍历堆
总结
- 优点
- 不需要移动对象
- 利用了空间和时间的局部性
- 缺点
- 空闲空间的组织方式
- 这也是标记清扫算法除需要中断机器的最主要问题。 以Dalvik为例,其为了避免这个问题不是自己来管理一块大空间的内存,而是通过dlmalloc来接管下面的数据。 所以其关键就是dlmalloc对于内存布局的合理分配。
- 碎片较多
- 不同工作集的数据交织在一起
- 影响了空间局部性
- 空闲空间的组织方式
标记缩并
碎片现象
制作符合目标的分配器: * 伙伴系统 * 两级分配 * 堆不必连续 * 缓解碎片 * 首次匹配 * 最佳匹配
缩并的方式
考虑因素
- 处理不同大小的对象
- 为了重新安放对象和更新指针,算法需要遍历堆几次
- 算法需要多少额外的空间
- 算法是否需要对指针施加某种限制(内部指针、回指)
- 每一步需要做多少工作
单元排序方式
- 滑动的: 将活动单元滑动到堆的一端,把存活单元之间的自由单元“挤出去”,从而维持了分配是的原始次序。
- 线性: 尽可能的将原来的单元和他所指向的单元放在相邻的位置。例如:以深度优先次序复制图中的节点的节点复制器可以利用类似cdr-coding这样的技术对数据结构进行压缩
- 任意
三个阶段
- 标记存活的数据结构
- 通过重新安防单元来缩并这些单元构成的图
- 更新指向被移动了的单元的指针
现有的算法
双指针算法:使用两个指针,一个指向下一个空闲的位置,另一个指向下一个要移动的存活单元。单元被移动之后,在他们原先的位置上留下一个迁移地址。这类方法一般只能适用于固定大小的单元。
迁移地址算法:在移动单元之前,把迁移地址写入每个单元的一个附加区域中。这一类方法可以用于收集不同大小的节点。
基于表的方法:在重新安放单元之前或是与之同时,在堆中构造一个存放重新安放单元的相关信息的映射表。这个表通常被称为间断表。算法将在以后查阅这个表,借助它计算指针的新值。
穿线方法:每个单元被链入一个链表,其中包含了所有原先指向他的单元。当单元转移时,算法通过这个链表做调整。
Lisp2(forward指针)
- 算法是一种滑动缩并算法,需要在每个对象的头部添加一个指针域forwarding用于存放该对象下一个新位置,并且需要进行三次堆扫描:
- 计算存活对象的新位置并存到forwarding:其实就是移动算法。从heap起始位置做紧凑性压缩准备,对象新地址基于当前已占用空间的大小
- 更新存活对象的指针域==指向对象的forwarding域
- 存活对象基于forwarding移动
- Forwarding指针
- 三遍扫描
穿线方法
将指向同一个地址的指针利用链表(已有链表)串联起来,然后等新数据确定后再更新数据。 1. A->P, B->P, C->P 1. P==Data 1. P-> C->B->A==Data 1. 在P更新地址后,在遍历链表更新数据。
Jonkers
- 操作本对象时涉及其他对象(工作集外面)
- 穿线工作量很大
- 该算法的限制:
- 指针只能指向单元的头部
- 单元头的大小必须足以存放一个地址
- 单元头必须包含能够同指向堆的指针区分开来的数据
- 具体:
- 两次遍历:
- 处理前向指针(forward pointer):较低地址指向较高地址的指针
- 处理后向指针(backward pointer):较高地址指向较低地址的指针
- 这个模型也需要看系统体系结构中数据的组织形式,是否有一个可以容纳空间的对象头。
- 两次遍历:
基于表的方法
- 利用空闲区域存放: 利用堆空间自身,做迁移数据的保存。可以说是对forwarding的改进:不需要单独的存储空间。
- 间断表: 在空闲区域存放间断表,间断表记录:(存活块的起始位置,到目前空闲块时已经发现的空闲空间大小)。 在完成扫描后可以基于该表进行指针更新。
双指针算法
该算法首先标记存活数据结构,并记录存活单元的数量,结果记为nlive 1. 第一次遍历将堆中较高的部分(高于Heap[nlive])的单元重新安放到较低部分的空洞里,并把迁移地址写入腾出的空间的第一个区域。 1. 第二次遍历扫描堆中较低的部分中的单元(到Heap[nlive]为止),更新其中的指针并使之指向新的位置。
在这里双指针的概念主要是指----算法使用两个指针: 1. free从堆的底部开始扫描,寻找自由节点 1. live则从堆的顶部开始扫描,寻找存活单元 1. 当free==live时第一次堆扫描完毕
重新安放单元的次序是随机的.
处理相同大小的数据单元
- 多次alloc,不同大小分配于不同堆区
- 将可变大小的数据缩并到堆中干净的页面中去:
- Bartlett:
- 当整个堆被填满的部分超过85%时,它被用来追踪和缩并最年老的分代。
- 核心想法:把堆Heap分成固定大小的块block。有自由(空闲)块、和其他已经活动的块。
- 对象从当前的自由块中分配,为此只需要增大一个指针。
- 在缩并阶段,尽可能地减少在块之间移动的数据量,用轻微的碎片现象换取减少移动。其主要是缩并单个块而不是整个堆。
- 第一次扫描,寻找不到1/3满的块,把这些块上被标记的单元移动到当前自由块中,并留下一个迁移地址;而更满的块不会被处理。
- 队列中还有另外一个备用的自由块,用于防止自由块出现溢出。
- 第二次扫描时,和双指针算法相同进行指针更新处理。
- 利用自由块作为一层中间数据,缩并时先使用自由块空间。
- 块分为:自由块(空闲块)、刚刚进行分配使用的块(新对象)、发生了归并处理的块(对象已经移动,块中包含了迁移地址,不能被直接拿去使用)、归并处理结束包含数据已经完全没用的块
- Dalvik Copying算法基本上是使用了这种思想。
- Bartlett:
指针域更新问题
- 动态更新所有指针域
- 利用对象句柄
节点复制垃圾收集
如果存活数据在整个堆中只占很小的一部分,那么节点复制技术就特别有吸引力。
利用多余的连续空间,将数据从一端完整的滑向另一端,原数据的不变性也保证了指针更新。
三色法思路,scan&free指针
对象重组
主要是提高空间局部性,这主要由: * 对象访问模式 * 对象组织模式 * 对象内部数据指针指向模式等具体情况
多区域收集
大型对象区域
大型对象利用单独的存储空间进行处理,以减少移动带来的开销。
最好这些大型对象时原数据,即像图像这种不带有指针的数据。
甚至于可以和支持虚拟内存系统的OS做交互,修改页表映射做处理。
渐进的递增缩并垃圾收集
将数据分成很多小的block,然后每次只对一部分block做归并收集。
静态区域
节点复制的效率
通过扩展堆的大小,可以任意地降低节点复制垃圾收集的平均成本。
但是和标记清扫收集,其空间局部性很差。主要是由于整个数据从一个半区T移动到另外一个半区F。 可能F需要被重新换入主存,而T在主存的优势也完全丧失了。
双边收集(top&bottom)
分代垃圾收集
分代的代提升策略: * 多个分代 * 利用阈值判断 * 自适应
分代组织和年龄纪录: * 每个分代一个半区 * 创建一个附属空间 * 记录年龄 * 大型对象区域
分代间指针的管理:主要是涉及到mark时根节点集合(只会扫描该分代对象,较老分代的引用就需要作为根节点处理): * 拦截器 * 各个分代一个入口表 * 记忆集 * 顺序保存缓冲区 * 硬件支持的页面标记 * 虚存系统支持的页面标记 * 卡片标记 * 记忆集与卡片可以相互结合
分代与对象生命周期
对象的生命周期与特定应用场景有关,并不能够通过一种简单的途径假设。
目前已经普遍存在的假设: * 强分代假设:越老的对象与不可能死亡。这种假设一般是不成立的。 * 弱分代假设:大多数对象在年轻时死亡
有些比较可观的结论: * 对象生命周期的分布是"成块"的。
目前CRuby已经在2.1.0上采用了混合MarkSweep与分代式GC的策略. Ruby2.1.0GC 其中有一些GC方面的分析技术与手段思路还是可以借见的.
渐进式和并发垃圾收集
主要是进行并发时的保护,主要是通过拦截器完成。
借助三色方案~
MarkSweep
- Dalvik MarkSweep
- Write-Barrier
- dvmWriteBarrierField
- dvmSetFieldObject
- createArrayClass
- loadClassFromDex0
- dvmLinkClass
- dvmInitClass
- setInstFieldValue
- SET_TYPE_FIELD(Jni.cpp)
- enqueuePendingReference
- dequeuePendingReference
- clearReference
- enqueueFinalizerReferences
- DVM_OBJECT_INIT
- dvmGenerateProxyClass
- proxyConstructor
- dvmPrepMainThread
- dvmCreateInterpThread
- dvmAttachCurrentThread
- dvmDetachCurrentThread
- makeStringObject
- dvmSetFieldObjectVolatile
- setInstFieldValue
- SET_TYPE_FIELD(Jni.cpp)
- dvmSetFieldStaticObject
- dvmSetFieldStaticObjectVolatile
- Dalvik_sun_misc_Unsafe_compareAndSwapObject
- Dalvik_sun_misc_Unsafe_putObjectVolatile
- Dalvik_sun_misc_Unsafe_putObject
- Dalvik_sun_misc_Unsafe_putOrderedObject
- dvmSetFieldObject
- dvmWriteBarrierObject
- dvmWriteBarrierArray
- goto_target
- Dalvik_java_lang_System_arraycopy
- dvmSetObjectArrayElement
- dvmGetInterfaces
- dvmWriteBarrierField