G1 收集器

基础知识

性能指标

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

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

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

 吞吐量Throughput 专一于最大程度地提升应用程序在特定时间段内的工做量:算法

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

较长的暂停时间Pause Time对于注重响应性的应用程序是不可接受的,但对于注重吞吐量的应用程序来讲能够接受的。前者重点是在短期内作出响应,后者则侧重与长时间运行的处理效率。数据库

GC 基础

GC Root

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

能够做为 GC Roots 的对象:缓存

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

三色标记

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

每一个对象可能为如下 3 种颜色之一:数据结构

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

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

遍历完成以后全部可达对象都是 black 的,此时全部标记为 white 的对象都是能够回收的。oracle

当实现并发标记算法时,必须防止 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 (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 (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,其中必然包含全部 Young 区域,同时可能包括部分 Old 区域:

根据回收区域的不一样,能够将 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 (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 (STW) 从新标记

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

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

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

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

  • Cleanup 清理垃圾

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

    主要工做有:

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

    此外还会执行一些清理工做,为下一次 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 信息插入一个 DCQ Dirty Card Queue,而后由 Refine 线程将其拾取并将信息传播到被引用区域的 RSet。

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

总结

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

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

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


参考资料

相关文章
相关标签/搜索