垃圾收集器与内存分配策略--垃圾收集算法

1 垃圾收集算法

1.0 分代收集理论

在介绍具体算法前,我们需要先了解大部分主流垃圾收集器的共同设计理论“分代收集”。

  • 垃圾收集专业术语

    • 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
      • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
      • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。
      • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
    • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
  • 核心假说

    • 弱分代假说:绝大多数对象都是朝生夕灭的
    • 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
  • 设计原则:基于核心假说

    • 收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。
  • 分代收集具体流程与问题

    • 流程:基于现在的商用Java虚拟机
      • 至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域
      • 在对新生代进行Minor GC时,每次垃圾收集时都会有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
    • 问题:对象不是孤立的,对象之间会存在跨代引用。例如新生代中的对象被老年代所引用:
      • 此时,为了找出老年代中的存活对象,不得不在固定的GC Roots之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,反之亦然(理论允许,但实际上除了CMS收集器,其他都不存在只针对老年代的收集)。遍历整个额外区域的所有对象将会为内存回收带来很大的性能负担。
  • 解决问题所需的经验假说

    • 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

      • 此为核心假说的推论:存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。

  • 具体解决流程:基于经验假说

    • 为了少量的跨代引用不必去扫描整个老年代,也不必浪费空间记录每一个对象是否存在以及存在哪些跨代引用。
    • 只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),此结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。
    • 此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。
    • 虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记忆集的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。

本节介绍的所有算法均属于追踪式垃圾收集(Tracing GC)的范畴

1.1 标记-清除算法

最早出现且最基础的垃圾收集算法

在这里插入图片描述

  • 基本原理:该算法分为两个阶段,标记(Mark)和清除(Sweep)。

    • 标记阶段:遍历所有对象,标记出所有需要回收的对象(或者标记所有存活的对象)。
    • 清除阶段:回收所有被标记的对象(或所有未标记对象)。
  • 主要缺点

    • 执行效率问题:标记和清除过程的效率可能不稳定,特别是当存在大量需要回收的对象时,这两个过程需要处理大量的对象,导致效率降低。

    • 内存碎片化:清除过后会产生许多小的、不连续的内存碎片。随着时间的推移,这些碎片可能导致无法为较大的对象找到足够的连续内存空间,从而可能触发更频繁的垃圾收集。

      后续的垃圾收集算法大多都是在标记-清除算法的基础上提出的,目的就是为了解决其效率不稳定和内存碎片化的问题。

1.2 标记-复制算法

主要用于新生代垃圾回收的垃圾收集算法

在这里插入图片描述

  • 基本原理:将可用内存划分为两块相等的区域,每次只使用其中一块。当这一块的内存用完时,将存活的对象复制到另一块上,然后清理掉已使用过的内存区域。
  • 优点:实现简单,运行高效,同时解决了当大部分对象为可回收时执行效率不稳定和内存碎片化的两个问题。
  • 缺点:空间利用率较低,因为任何时候只有一半的内存区域被使用。并且当大部分对象存活时性能将大大降低
  • 特殊优化:Appel式回收
    • 内存划分:新生代被分为一块较大的Eden空间和两块较小的Survivor空间。
    • 基本原理:每次使用Eden和一块Survivor进行内存分配。垃圾回收时,将存活对象从Eden和一块Survivor复制到另一块Survivor上。绝大部分新生代对象都在一次垃圾回收中被清除。
    • 内存比例:默认Eden和Survivor的比例为8:1,即90%的新生代可用内存(Eden加一个Survivor)。
    • 内存分配担保:Appel式回收的安全机制
      • 触发条件:当空闲的Survivor空间不足以容纳一次新生代回收后的存活对象时。
      • 机制:这些对象会直接被分配到老年代中,类似于银行借款的担保机制。
1.3 标记-整理算法

在这里插入图片描述

  • 主要应用于老年代:由于老年代的对象存活率较高,标记-复制算法在此不太适用(内存担保将会频繁触发),而更具针对性的标记-整理算法也可以有效地解决内存碎片化的问题,同时避免了空间的浪费。
  • 基本原理:首先标记出所有存活的对象,然后将所有存活的对象移动到内存的一端,最后清理掉边界以外的内存。
  • 目的:解决了标记-清除算法中的内存碎片化问题。
  • 非移动式 vs 移动式:标记-清除是非移动式的回收算法,而标记-整理是移动式的。
    • 停顿时间 vs 吞吐量:移动对象会增加垃圾收集的复杂度和停顿时间(STW),但有利于提高程序整体的吞吐量。
  • 平衡策略:不同的垃圾收集器会根据各自的侧重点,在标记-清除和标记-整理算法之间进行选择或切换。
  • 具体实现
    • Parallel Scavenge收集器(关注吞吐量)基于标记-整理算法。
    • CMS收集器(关注延迟)基于标记-清除算法,但在空间碎片化严重时会采用标记-整理算法。

2 HotSpot的可达性算法细节实现

2.1 根节点枚举
  • 定义:在垃圾收集的可达性分析中,先要通过根节点枚举找到GC Roots集合。
  • Stop The World:所有垃圾收集器在根节点枚举时必须暂停用户线程,以保持引用关系的一致性,防止在分析过程中引用关系变化。此时,执行子系统看起来被冻结。
  • 挑战:在大型Java应用中,光是方法区就非常庞大,包含大量类和常量,逐个检查哪些常量或静态变量为引用类型(可作为GC Roots)是非常耗时的。
  • HotSpot虚拟机的优化实现
    • 解决方式:使用一组称为OopMap的数据结构来记录引用对象的位置:类加载动作完成时, HotSpot会把对象内什么偏移量上是引用类型的数据计算出来(与根节点枚举关系不大),并且在即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用(加快了针对执行上下文根节点枚举)。
    • 高效枚举:借助OopMap,垃圾收集器在扫描时可以直接知道在执行上下文中哪些位置存储了对象引用,无需逐个查找。
2.2 安全点
  • OopMap的优势与问题:

    • 优势:HotSpot使用OopMap来准确快速地完成GC Roots的枚举。
    • 问题:但由于可以导致引用变化的指令众多,为每条指令都生成OopMap会导致巨大的存储空间消耗,从而增加垃圾收集的空间成本。
  • 解决方案:采用**“安全点”**

    • 具体方式:不为每条指令生成 OopMap,而是只在特定位置,即“安全点”,进行记录。
    • 选择依据:安全点的选取基于指令是否会导致程序长时间运行(如方法调用、循环跳转等),并在这些“长时间运行”的指令序列中选取特定位置安插安全点(这些位置是在JIT编译器和运行时系统中明确定义的)。
    • 平衡考量:安全点数量需平衡,既不能太少(避免长等待),也不能太多(防止过大内存负担)。
  • 多线程暂停机制

    • 目的:在垃圾收集时,确保所有线程都能在安全点暂停。
    • 方法一:抢先式中断:系统中断所有线程,非安全点上的线程恢复直到到达安全点。目前几乎不使用。
    • 方法二:主动式中断:通过设置中断标志位(类似于while循环中的“开关变量”),线程轮询此标志并在标志位为true时主动在最近的安全点暂停。

    4. 轮询操作的优化:基于HotSpot

    • 需求:由于轮询操作频繁,必须高效。
    • 实现方式:HotSpot 采用内存保护陷阱,将轮询操作精简为一条汇编指令。
    • 执行机制:当需要垃圾回收时,设定标志位为true。此时,使任意线程执行轮询操作时设定包含标志位的内存页不可读来触发异常。因此,所有线程在后续执行轮询操作时都会产生异常(异常抛出为“长时间运行”的指令序列,可以插入安全点)并暂停线程。
2.3 安全区域
  • 安全点机制的限制

    • 问题:虽然安全点机制确保了在程序执行时能够及时暂停线程并进入垃圾收集状态,但它并未解决程序“不执行”时的情况:当线程处于Sleep状态或Blocked状态,它们无法响应虚拟机的中断请求,因而无法移动到安全点进行暂停。
  • 解决方案:采用安全区域(Safe Region)

    • 定义:安全区域是一段代码段,在这段代码段内的任意位置,引用关系都不会发生变化,从而使得在这个区域内的任何地方开始垃圾收集都是安全的。
    • 与安全点的关系:可以视为将安全点的概念扩展到一段代码区域。
  • 具体流程

    • 进入安全区域:当线程执行到安全区域内的代码时,它标识自己已经进入安全区域,这样虚拟机在进行垃圾收集时就不必考虑这些线程。

    • 离开安全区域

      线程在离开安全区域之前,需检查虚拟机是否已完成根节点枚举或其他需要暂停用户线程的垃圾收集阶段。

      • 如果此垃圾收集阶段已完成:线程继续执行
      • 如果此垃圾收集阶段未完成:线程必须等待,直到收到可以离开安全区域的信号。
2.4 记忆集与卡表

在这里插入图片描述

  • 记忆集的作用与问题
    • 作用:分代收集理论中,为解决对象跨代引用问题,采用记忆集来记录从非收集区域指向收集区域的指针。
    • 问题:记录所有含跨代引用的对象会造成非常高的空间占用和维护成本。
  • 记忆集的实现精度
    • 字长精度:记录精确到一个机器字长,包含跨代指针。
    • 对象精度:记录精确到一个对象,该对象中有字段含跨代指针。
    • 卡精度:记录精确到一块内存区域,区域内的对象含有跨代指针。这是最常用的实现形式。
  • 卡表(Card Table)
    • 定义:卡表是实现记忆集的一种具体方法,它决定了记忆集的记录精度和与堆内存的映射关系(类似于内存块(卡页)的索引)。
    • 实现:卡表通常实现为字节数组,每个元素对应一块特定大小的内存块(卡页)。
    • 功能:如果卡页内至少有一个对象存在跨代指针,相应的卡表数组元素标记为1(脏);否则标记为0。
    • 应用:垃圾收集时,通过筛选脏元素,快速确定包含跨代指针的卡页,将它们加入引用链进行搜索(卡页中的对象不在回收范围内,也不作为GC roots本身,只是用于引用搜索)。
2.5 写屏障
  • 卡表元素何时变脏

    • 触发时机:当其他分代区域中的对象引用了本区域的对象时,相应的卡表元素应变脏。理想情况下,这应该发生在引用类型字段赋值的那一刻。
  • 如何变脏(维护卡表):写屏障

    • 定义与作用:写屏障是一种虚拟机层面的技术,用于在引用类型字段赋值时执行额外动作。它类似于面向切面编程(AOP)中的环绕通知。

    • 类型:分为写前屏障(Pre-Write Barrier)和写后屏障(Post-Write Barrier)。

    • HotSpot中的应用:在HotSpot中,直至G1收集器出现之前,其他收集器都只用到了写后屏障。

  • 具体实现

    • 在引用字段赋值后,执行写后屏障部分的逻辑来更新卡表状态。

      虽然写屏障会为每次引用更新增加额外开销,但这比Minor GC时扫描整个老年代的代价要低。

2.6 并发的可达性分析
  • 可达性分析的理论要求

    • 需要在一个一致性的快照上进行分析,通常需要冻结用户线程。
  • 并发可达性分析的问题

    • 主要挑战: 在并发环境下,用户线程和垃圾收集器同时工作,可能导致对象的引用状态改变(进行可达性分析时的对象图因而改变)。

    • 后果

      • 将死亡对象误标为存活(浮动垃圾,较能容忍)。

      • 将存活的对象误标为死亡,此事件发生需同时满足两项条件(致命错误):

        • 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
        • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

        在这里插入图片描述

  • 三色标记法

    • 目的: 为并发环境下的对象状态进行辅助标记。
    • 标记颜色
      • 白色:未被访问的对象。
      • 黑色:已被访问,且所有引用都已扫描的对象。
      • 灰色:已被访问,但至少有一个引用未被扫描的对象。
  • 解决并发问题的策略

    • 基本思路:破环导致将存活的对象误标为死亡的两项条件的其中之一即可。

    • 增量更新(Incremental Update)

      在这里插入图片描述

      破坏条件:赋值器插入了一条或多条从黑色对象到白色对象的新引用;

      • 具体流程
        • 当黑色对象插入新的指向白色对象的引用时,记录这个引用,并且在记录时将黑色对象变为灰色。
        • 并发扫描结束后,将这些记录的引用重新扫描一次。
    • 原始快照(Snapshot At The Beginning, SATB)

      在这里插入图片描述

      破坏条件:赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

      • 具体流程:(结合3.7.7)
        • 当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来(实际上会记录所有被删除的指向白色对象的引用关系),并且在记录时将旧引用标为灰色。
        • 在并发扫描结束之后,再将这些记录的引用重新扫描一次
    • 原始快照产生浮动垃圾的原因是无视了引用的删除和改变,而增量更新则是因为对象被标记为黑色后无法察觉引用的删除

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值