字节跳动青训营笔记-bitdance--gc垃圾回收

244 阅读8分钟

[这是我参与「第四届青训营 」笔记创作活动的第7天]

算法分类

  • 按确定对象是否存活分:引用计数 GC 和追踪式 GC(可达性分析)
  • 按是否确定是引用类型分:保守式(Conservative)GC 和精确式 GC

精确式 GC 是指在回收过程中能准确的识别和回收每一个无用对象的 GC 方式,为了准确识别每一个对象的引用,通常要求一些额外的数据,这些数据通常对用户程序是透明的。

和精确式 GC 相反,保守式 GC 不能准确的识别每一个无用对象,但是能保证在不会错误的回收存活的对象的情况下回收一部分无用对象。保守式 GC 并不需要额外的数据来支持查找对对象的引用,它将所有内存数据假定为指针,通过一些条件来判定这个指针是否是一个合法的对象的引用。

  • 搬迁式和非搬迁式 在GC过程中是否需要移动对象的内存位置。
  • 实时和非实时GC 是否需要停止用户程序执行的GC方式
  • 渐进式和非渐近式GC 渐进式 GC 和实时 GC 一样不需要中断用户程序运行,不同的地方在于渐进式 GC 不会在对象抛弃时立即回收占用的内存资源,而是在达成 GC 条件时统一进行回收操作。

算法

引用计数式

通过额外的计数域来实时计算对单个对象的引用次数,当引用次数为 0 时回收对象。引用计数式GC是实时的。

优点

  • 引用计数方法是渐进式的,它能及时释放无用的对象,将内存管理的的开销实时的分布在用户程序运行过程中。
  • 实现简单

缺点

  • 更改对象引用的时候需要调整新旧两个对象的引用技术,增加了指针复制的成本

  • 需要额外的空间保存计数值,要求框架和编译器支持

  • 最严重的问题是循环引用问题

    解决方案:

    • 弱指针 弱指针算法使用两个计数域来计算对对象的引用,一个称为强引用,一个称为弱引用。当强引用计数为 0 时对象不再可用。弱指针算法必须小心的维护弱引用,如果出现两个强互相引用,依然难以避免环形引用问题。

追踪式 GC

追踪式 GC 算法通过递归的检查对象的可达性来判断对象是否存活,进而回收无用内存。这类算法的共同优点是不必考虑循环引用的问题。

标记清理(Mark Sweep)

通过搜索整个系统中对对象的引用来检查对象的可达性,以确定对象是否需要回收。实现可为保守或者精确式。

过程 标记清理算法可以分为两个阶段:标记阶段清理阶段。 标记阶段从 根节点集合(GC Roots) 开始递归标记所有可达对象,根结点集合通常包括 方法区中常量引用的对象方法区中静态属性引用的对象虚拟机栈中本地变量表中引用的对象本地方法栈中 JNI 引用的对象,这些数据可以被用户程序直接或者间接引用到。 清理阶段遍历所有对象,将没有标记为可达的对象回收,并清理标记位。

保守式的标记清理算法: 因为缺少对象引用的内存信息,嘉定所有根节点集合为指针,递归将这些指针指向的内存区域标记为可达,并将所有可达区域的内存数据假定为指针,最终识别出不可达的内存区域并回收。 很明显这样可能导致内存泄露,加入讲一个整型值当做一个指针,并且这个值碰巧指向一个已经分配的堆内存,这将会导致这部分内存被标记为可达而不能被回收。

优点

  • 操纵指针时没有额外的开销
  • 与用户程序完全分离

缺点

  • 标记清理过程需要暂停用户程序运行
  • 产生大量内存碎片

标记压缩/整理(Mark-Compaction)

内存碎片增加了查找可用内存区域的开销,标记缩并算法就是为了处理内存碎片问题而产生的。

过程 标记整理算法分为三个阶段:标记阶段整理/压缩阶段更新阶段

  • 标记存活的对象
  • 移动对象病合并空闲区块
  • 更新所有到存活对象的引用

优点

  • 减少了内存碎片,提高了内存分配和访问效率
  • 相比于复制算法,内存利用率更高

缺点

  • 需要移动对象位置和更新引用,GC 时间更长
  • 需要保存额外的压缩信息
  • 需要精确识别对象引用

压缩算法的两种实现:

  • 双指针算法 双指针算法要求每次分配的对象大小必须一样,但是并不需要额外的数据结构来保存节点信息

    • (1)Free 指针从从堆末尾查找空闲节点,Live 指针从堆顶查找存活节点
    • (2)将 Live 指针指向的存货节点复制到 Free 指针指向的空闲节点,将 Free 指针的地址写入到 Live 指针指向的位置
    • (3)移动 Free 指针和 Live 指针,重复上一步知道两个指针相遇
  • 迁移地址算法 迁移地址算法适用于可变大小的内存分配,但要求对象中包含一个记录对象新位置的字段,并且需要遍历三次堆

    • 第一次从堆头部开始遍历,计算到当前位置遇到的所有的存货对象的大小(不包括当前对象),将值记入当前对象的新位置字段,同时将相邻的空闲字段合并,以减少后面遍历的次数
    • 第二次遍历所有的对象,将对其它对象的引用更新到新位置
    • 第三次移动所有对象到新位置,清楚新位置字段的值,为下次 GC 做准备

复制算法

节点复制GC通过将所有存活对象从一个区移动到另一个区来过滤非存活对象。

优点

  • 节点复制算法的开销正比于存活数据的容量,而不是整个堆的大小。
  • 减少了内存碎片,有更好的内存局部性。
  • 新对象的分配更简洁高效,并且不需要维护空闲块的列表等辅助数据结构。
  • 在低对象存活率的环境中有更高的效率。

缺点

  • 需要双倍的内存
  • 大型对象的复制消耗可能很大

因为大部分对象的生命周期都比较短,因此没必要按 1:1 的比例划分空间,而是划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次只是用 Eden 和其中一块 Survivor 空间,回收时复制到另外一块 Survivor 空间。 当 Survivor 空间不足的时候,就需要另外的空间进行分配担保

分代式GC(Generational Garbage Collection)

在程序运行过程中,许多对象的生命周期是短暂的,分配不久即被抛弃。因此将内存回收的工作焦点集中在这些最有可能是垃圾的对象上,可以提高内存回收的效率,降低回收过程的开销,进而减少对用户程序的中断。 分代式GC每次回收时通过扫描整个堆中的一部分而是不是全部来降低内存回收过程的开销。

记忆集 记忆集是为了提高新生代回收效率而用来保存新生代的根节点的集合,包括从这个分代区域外到这个分代区域的所有对象的引用,这些引用可能来自方法区静态变量、栈、和老年代。其中老年代到新生代的引用来源有两个,一个是对象从新生代提升到老年代之后维持着原来的引用,另一个是运行时修改了老年代对象的字段,引用了一个新生对象,前一种可以由垃圾收集器回鹘,后一种需要拦截器来维护。

拦截器 拦截器通常是一小段内联代码,在修改对象引用时执行一些特殊操作以保证 GC 的正确执行。 拦截器分两种,写拦截器和读拦截器,写拦截器保证了用户成许修改对象引用时将修改记录下来,如放到记忆集中,读拦截器保证了用户程序访问到的对象都是可达的,如果还没标记为可达,立即标记它。

优点

  • 只收集堆的一部分,减少了内存回收的开销,缩短了用户程序的中断时间

缺点