zoukankan      html  css  js  c++  java
  • G1 收集器

    基础知识

    性能指标

    在调优Java应用程序时,重点通常放在两个主要目标上:响应性吞吐量

     响应性Responsiveness 是指应用程序对请求的数据做出响应的速度:

    • 桌面用户界面对事件的响应速度
    • 网站返回页面的速度
    • 数据库查询的返回速度

     吞吐量Throughput 专注于最大程度地提高应用程序在特定时间段内的工作量:

    • 在给定时间内完成的事务次数
    • 批处理程序在一小时内可以完成的作业数
    • 一小时内可以完成的数据库查询数

    较长的暂停时间pause time对于注重响应性的应用程序是不可接受的,但对于注重吞吐量的应用程序则无伤大雅。前者重点是在短时间内做出响应,后者则侧重与长时间运行的处理效率。

    GC 基础

    GC Root

    可达性分析是 Java GC 算法的基础,基本思路就是以一系列名为 GC Roots 对象作为起始点,通过引用关系遍历对象图,如果一个对象到 GC Roots 间没有任何可达路径相连时,则说明此对象可以被回收。

    可以作为 GC Roots 的对象:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 本地方法栈中JNI(即一般说的native方法)中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象

    三色标记

    可达性分析中重要的一环就是遍历整个堆,并标记其中的存活对象。一种常用的标记算法是 三色标记法tri-color marking

    每个对象可能为以下 3 种颜色之一:

    • white — 未被标记
    • gray — 本身已标记,但部分引用的对象未被标记(动图的黄色对象)
    • black — 本身已标记,且所有引用的对象完成标记(动图的蓝色对象)

    标记算法从 GC Roots 出发遍历堆,可达对象先标记 gray,然后再标记 为 black。

    遍历完成之后所有可达对象都是 black 的,此时所有标记为 white 的对象都是可以回收的。

    当实现并发标记算法时,必须防止 white 对象被漏标,否则可能导致不该回收的对象被回收。


    分代收集

    传统垃圾收集器将堆分成三个部分:年轻代YoungGen = Eden + Survivor,老年代OldGen和永久代PermGen,每个区域内存连续且大小固定。

    • 年轻代:一次性使用的临时对象(例如:方法中构造的临时对象)
    • 老年代:被长期引用的常驻对象(例如:缓存对象、单例对象)
    • 永久代:JVM 运行过程中一直存在的对象(例如:字符串常量、类信息)

    将堆内存进行划分后,可以按照对象生命周期长短,在不同区域使用不同的回收算法,提高 GC 的效率。


    算法分类

    Mark and Sweep标记-清除

     用一个空闲列表free-list记录失效对象占用的内存区域,方便后续重新分配给新对象。

    • 回收原理简单,GC 停顿时间短
    • 维护空闲列表需要一定的空间开销
    • 内存碎片较多,可能导致内存分配失败

    Mark-Sweep-Compact标记-整理

     将所有存活对象移动到内存区域的开头,剩余的连续内存区域都是可用的空闲空间。

    • 通过指针碰撞查找空闲空间,分配速度快
    • 内存碎片少,内存分配失败概率低
    • 复制对象会导致较长时间的 GC 停顿

    Mark and Copy标记-复制

     将内存划分为活动区间空闲区间,前者用于动态分配对象,后者用于容纳 GC 存活对象。
     GC 时只需将存活对象从前者复制到后者,然后交换两者的角色即可。

    • 标记和复制在同一阶段同时进行,当存活对象少时回收效率极高
    • 需要预留一个空闲空间用于容纳存活对象,造成内存浪费

    CMS 回顾

    CMS Concurrent Mark-Sweep 是一个采用 标记-清除 算法的老年代收集器。
    它通过与应用程序线程并发执行大多数垃圾回收工作,来最大程度地减少由于 GC 导致的暂停。

    通常情况下,CMS 收集器不会复制或压缩活动对象,这意味着无需移动活动对象即可完成垃圾回收。
    然而过多的内存碎片可能造成分配失败,最终导致 FullGC。可以通过分配更大的堆来规避这一问题。

    CMS 对老年代的回收可以分为以下几个步骤:

    • Initial Mark ( iny extsf{(STW)}) 初始标记

      • 标记 GC Roots 直接可达的老年代对象
      • 遍历新生代存活对象,标记直接可达的老年代对象

    • Concurrent Mark 并发标记

      GC 线程遍历 Initial Mark 阶段标记出来存活的老年代对象,然后递归标记这些可达的对象。

      该阶段与应用线程并发运行,期间会发生新生代对象晋升、老年代对象引用关系更新,需要对这些对象进行重新标记,避免发生遗漏。

      CMS 用一个card-table管理老年代,并发标记过程中,某个对象的引用关系发生了变化,则将对象所在的内存块标记为 Dirty Card

      CMS 使用增量更新incremental update解决并发修改导致的漏标问题:把 black 对象重新标记为 grey,下次重新扫描其引用。

    • Preclean 预清理

      这一阶段主要是处理 Concurrent Mark 阶段中引用关系改变,导致没有标记到的存活对象的。通过并发地重新扫描这些对象,预清理阶段可以减少 Remark 阶段的 STW。

      这个阶段会处理前一个阶段被标记为 Dirty Card 的部分,将其中变化了的对象作为 GC Root 再进行扫描并重新标记。

    • Abortable Preclean 可终止的预清理

      这个阶段作用与 Preclean 类似,但可以通过设置 扫描时长(默认5秒)或 Eden 区使用占比(默认50%)控制本阶段的结束时机。

      增加这一阶段的原因,是期待这期间能发生一次 YoungGC 清理无效的年轻代对象,减少 Remark 阶段扫描年轻代的时间。

    • Remark ( iny extsf{(STW)}) 重新标记

      这个阶段同时扫描 YoungGen 与 OldGen,重新标记整个老年代中所有存活对象。

      由于之前的 Concurrent MarkPreclean 阶段是与用户线程并发执行的,年轻代对老年代的引用可能已经发生了改变,Remark 要花很多时间处理这些改变,会导致长时间的 STW。

      此外,即使新生代的对象已经不可达了,CMS 也会使用这些不可达的对象当做的 GC Roots 来扫描老年代,导致部分失效的老年代对象无法被及时回收。

      可以加入参数 -XX:+CMSScavengeBeforeRemark,在重新标记之前,先执行一次 YoungGC,回收掉年轻代的对象无用的对象。这样进行年轻代扫描时,只需要扫描 Survivor 区的对象即可,一般 Survivor 区非常小,这大大减少了扫描时间。

    • Concurrent Sweep 并发清理

      至此,老年代所有存活的对象已经被标记完成。这个阶段主要是清除那些没有标记的对象并且回收空间。

      被回收的空间会被添加到 空闲列表中,以供以后分配。这一过程可能会对空闲空间进行合并,但是不会移动存活对象。

      由于该阶段是与应用线程并发运行的,自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,无法在当次收集中处理掉它们。只好留待下一次GC时再清理掉。这一部分垃圾就称为 浮动垃圾

    • Resetting 重置

      清除数据结构,并重置定时器,为下一轮 GC 做准备。

    G1 算法

    设计目的

    G1 Garbage-First 是一种服务器端的垃圾收集器:

    • 可以与应用程序线程并行运行,减少 STW
    • 整理空闲空间减少内存碎片,但不引入较长的 GC 暂停时间
    • 提供可预测的GC暂停时间,无需牺牲很多吞吐量

    G1 能够在大内存的多处理器计算机上,保证 GC 暂停时间可控,并实现高吞吐量。

    其最终目的是取代 CMS 成为服务端 GC 更好的解决方案:

    • 采用 标记-整理 算法,可以避免使用细粒度的空闲列表进行分配。简化了收集器设计并消除了潜在的碎片问题。
    • 使用 增量回收incremental collecting 算法,其 GC 暂停时间比 CMS 更具可预测性,并允许用户指定期望的暂停时间。

    基本概念

    G1 将堆划分为一组大小相等的且连续的堆区域Region

    G1 中新生代与老年代不再连续,每个区域可以在 EdenSurvivorOld 之间切换角色。此外,还有一类被称为 Humongous 的巨型区域,用于容纳体积 ≥ 标准区域大小的50%的对象。

    JVM 通常会将内存划分为 2000个区域,每个大小从 1 到 32Mb 不等,由 JVM 在启动时通过 -XX:G1HeapRegionSize 指定。

    每个区域会被进一步细分成多个卡片Card,每个大小为 512Kb,用于实现细粒度的引用统计。

    分区设计可以避免一次收集整个堆,每次 GC 只收集区域的一个子集 CSetcollection set

    根据回收区域的不同,可以将 GC 分为:

    • YoungGCCSet 只包含 Young 区域
    • MixedGCCSet 同时包含 YoungOld 区域
    • FullGC: 回收整个堆(可用空间耗尽时触发,单线程执行)

    G1 根据存活对象的字节数统计每个区域的 活跃度liveness,然后根据期望停顿时间来确定该 CSet 的大小,并保证那些垃圾多(活跃度低)的区域会被优先回收,故此得名 垃圾优先

    G1 的执行过程可以表示为由 3 个阶段组成的循环:


    Young GC

    堆中一开始只有 YoungGen,因此只会触发 YoungGC,将 EdenSurvivor 区域中的活动对象复制到另一个空闲的 Survivor 区域。

    G1 中将 将存活对象复制到其他区域 的过程称为 疏散Evacuation。为了减少停顿时间,疏散工作由多个 GC 线程并行完成。

    YoungGC 过程中会根据预期目标停顿时间 -XX:MaxGCPauseMillis 动态调整新生代的大小,通过 -XX:G1NewSizePercent 参数可以人为干预这一过程,但会让预期停顿时间参数失效。

    当堆的整体占用空间足够大时(超过45%),就会进入 Concurrent Marking 阶段。通过 -XX:InitiatingHeapOccupancyPercent 选项可以配置这一行为。


    Concurrent Marking

    与 CMS 类似,G1 中的并发标记包括多个阶段,其中一些阶段是并发的,另一些阶段则会 STW。

    • Initial Mark ( iny extsf{(STW)}) 初始标记

      扫描并标记 GC Root 对象直接可达的老年代存活对象。

      Initial Mark 并没有独立的执行阶段,而是嵌入 YoungGC 中执行的,其停顿时间会被分摊,因此实际的开销非常低。


    • Root Region Scan 扫描根区域

      扫描 Root Region 并标记所有可达的老年代存活对象。

      此处的 Root Region 就是先前 YoungGC 中生成的 Survivor 区域,其包含的对象都会被视为 GC Root

      为了避免移动对象对标记产生影响,该过程必须在下次 YongGC 启动前完成。

    • Concurrent Mark 并发标记

      启动并发标记线程,扫描并标记整个堆中的存活对象(线程数可以通过 -XX:ConcGCThread 进行配置)。

      为了避免重复标记,G1 使用 SATBsnapshot-at-the-beginning算法解决漏标问题:

      应用线程对在 Concurrent Mark 执行期间进行的所有并发更新,都应保留先前的已知标记信息。

      该约束通过预写屏障pre-write barrier实现:

      Concurrent Mark 扫描过程中,当应用线程修改某个字段时,会将先前的引用对象存储在日志缓冲区log buffers中,然后交由并发标记线程处理。

      为了避免移动对象对标记产生影响,该过程必须在下次 YoungGC 启动前完成。所有的标记任务必须在堆满前完成,如果堆满前没有完成标记任务,则会触发担保机制,经历一次长时间的串行 FullGC

    • Remark ( iny extsf{(STW)}) 重新标记

      启动并行标记线程,完成对整个堆中存活对象的标记(线程数可以通过 -XX:ParallelGCThread 进行配置)。

      该阶段会暂停所有应用线程,避免发生引用更新,并完成对SATB 日志缓冲区中剩余对象的标记,找出所有未被访问的存活对象。

      该阶段还执行一些额外的清理操作,例如:

      • 卸载不可达的类(通过 -XX:+ClassUnloadingWithConcurrentMark 开启)
      • 处理引用对象(弱引用、软引用、虚引用、最终引用)

    • Cleanup 清理垃圾

      整理统计信息并识别出高收益的老年代分区,为 MixedGC 做准备。

      主要工作有:

      • RSet 梳理(后续说明)( iny extsf{(STW)})
      • 识别回收收益高的老年代分区(基于释放空间和暂停目标)( iny extsf{(STW)})
      • 直接回收完全没有活跃对象的空闲分区

      此外还会执行一些清理工作,为下一次 Concurrent Marking 做好准备。

    Mixed GC

    MixedGC 主要流程与 YoungGC 类似,不同的地方在于 CSet 中包含了 Old 区域。

    需要注意的是,Concurrent Marking 结束后,并不一定会立即触发 MixedGC,中间可能会穿插多次的 YoungGC

    当收集某个区域时,我们必须知道是否有来自非收集区域引用,来确定它们的活动性:

    • 从非收集区域到收集区域的 incoming reference 是重要的(被非收集区引用的对象必须存活)
    • 从收集区域到非收集区域的 outgoing reference 是可忽略的(非收集区域不参与GC)

    但查找整个堆非常耗时,同时也失去了增量收集的优势。为了解决这一问题,G1 为每个区域维护了一个 RSetremembered set,用于记忆从其他区域指向自己的引用。


    收集过程

    在执行收集时,RSet 中引用信息会扮演局部 GC Roots 的角色,避免耗时的引用查找,保证每个区域的 GC 能够独立进行:

    注意,象如果 Old 区域中对在 Concurrent Marking 阶段被确定为垃圾,即使有外部引用,该对象也会被作为垃圾回收。

    接下来发生的事情与其他收集器所做的相同:多个并行GC线程找出哪些对象是活动的,哪些对象是垃圾:

    最后,释放空闲区域,将活动对象移到 Survivor 区域,并在必要时创建新对象:


    RSet 维护

    为了维护 RSet,在应用线程对字段执行写操作时,会触发写后屏障post-write barrier

    如果更新后的引用是跨区域的(即从一个区域指向另一个区域),则对应的条目将出现在目标区域的 RSet 中。

    为了减少写屏障带来的开销,该过程是异步的:

    应用线程只负责把更新字段所在的 Card 信息插入一个DCQDirty Card Queue,然后由 Refine 线程将其拾取并将信息传播到被引用区域的 RSet。

    如果应用线程插入速度过快,会导致 Refine 线程来不及处理,那么应用线程将接管 RSet 更新的任务,从而导致性能下降。

    总结

    并发标记增量收集 是 G1 实现高性能与可预测回收的关键。

    对于 CPU 资源充足且对延迟敏感的服务端应用来说,G1 算法能够在大堆上提供良好的响应速度。

    作为代价,额外的写屏障与更活跃GC线程,会对应用的吞吐量产生负面影响。


    参考资料

  • 相关阅读:
    redis -- RDB
    redis--数据库
    redis-慢查询日志
    redis 订阅与发布
    redis 事务
    redis 事件
    redis--服务器与客户端
    readis 内部数据结构
    Redis数据类型
    Docker 限制容器资源
  • 原文地址:https://www.cnblogs.com/buttercup/p/13829881.html
Copyright © 2011-2022 走看看