关于后端:垃圾收集器必问系列G1

32次阅读

共计 4181 个字符,预计需要花费 11 分钟才能阅读完成。

本文已收录至 Github,举荐浏览 👉 Java 随想录

微信公众号:Java 随想录

CSDN:码农 BookSea

人生下来不是为了拖着锁链,而是为了开展双翼。——雨果

Garbage First(简称 G1)收集器是垃圾收集器技术倒退历史上的里程碑式的成绩,它创始了收集器面向部分收集的设计思路和基于 Region 的内存布局模式。设计者们设计 G1 的时候心愿 G1 可能建设起“进展工夫模型”,进展工夫模型的意思是可能反对指定在一个长度为 M 毫秒的工夫片段内,耗费在垃圾收集上的工夫大概率不超过 N 毫秒这样的指标。下文会有所讲述。

基于 Region 的堆内存布局

首先,介绍下 G1 基于 Region 的堆内存布局,这是可能可能建设起 “进展工夫模型” 的要害。

G1 逻辑上分代,然而物理上不分代

G1 不再保持固定大小以及固定数量的分代区域划分,而是把间断的 Java 堆划分为多个大小相等的独立区域(Region),每一个 Region 都能够依据须要,表演新生代的 Eden 空间、Survivor 空间,或者老年代空间。收集器可能对表演不同角色的 Region 采纳不同的策略去解决。

G1 能够通过参数管制新生代内存大小的参数:-XX:G1NewSizePercent(默认等于 5),-XX:G1MaxNewSizePercent(默认等于 60)。也就是说新生代大小默认占整个堆内存的 5% ~ 60%

这样就不存在界线,无论是新创建的对象还是曾经存活了一段时间、熬过屡次收集的旧对象都能获取很好的收集成果。

应用 G1 收集器时,它将整个 Java 堆划分成约 2048 个大小雷同的独立 Region 块,每个 Region 的大小能够通过参数 -XX:G1HeapRegionSize 设定,取值范畴为 1MB~32MB,且应为 2 的 N 次幂

能够简略算一下,G1 能治理的最大内存大概 32MB * 2048 = 64G 左右

Region 中还有一类非凡的Humongous 区域,专门用来存储大对象。G1 认为只有大小超过了一个 Region 容量一半的对象(即超过 1.5 个 region)即可断定为大对象。而对于那些超过了整个 Region 容量的超级大对象,将会被寄存在 N 个间断的 Humongous Region 之中,G1 的大多数行为都把 Humongous Region 作为老年代的一部分来进行对待。

可预测的进展工夫模型

G1 收集器之所以能建设可预测的进展工夫模型,是因为它将 Region 作为单次回收的最小单元,即每次收集到的内存空间都是 Region 大小的整数倍,这样能够有打算地防止在整个 Java 堆中进行全区域的垃圾收集。

G1 收集器会去跟踪各个 Region 外面的垃圾沉积的“价值”大小,价值即回收所取得的空间大小以及回收所需工夫的经验值,而后在后盾保护一个优先级列表,每次依据用户设定容许的收集进展工夫(应用参数 -XX:MaxGCPauseMillis 指定,默认值是 200 毫秒),优先解决回收价值收益最大的那些 Region,这也就是“Garbage First”名字的由来

这种应用 Region 划分内存空间,以及具备优先级的区域回收形式,保障了 G1 收集器在无限的工夫内获取尽可能高的收集效率。

所以说 G1 实现可预测的进展工夫模型的要害就是 Region 布局优先级队列。看起来如同 G1 的实现也不简单,然而其实有许多细节是须要思考的。

跨 Region 援用对象

G1 将 Java 堆分成多个独立 Region 后,Region 外面存在的跨 Region 援用对象如何解决?

解决方案的思路咱们曾经晓得,应用 记忆集

然而麻烦的是,G1 的堆内存是以 Region 为根本回收单位的,所以它的每个 Region 都保护有本人的记忆集,这些记忆集会记录下别的 Region 指向本人的指针,并标记这些指针别离在哪些卡页的范畴之内。

G1 的记忆集在存储构造的实质上是一种 哈希表,Key 是别的 Region 的起始地址,Value 是一个汇合,外面存储的元素是卡表的索引号。

因为 Region 数量较多,每个 Region 都保护有本人的记忆集,光是存储记忆集这块就要占用相当一部分内存,G1 比其余圾收集器有着更高的内存占用累赘。依据教训,G1 至多要消耗大概相当于 Java 堆容量 10% 至 20% 的额定内存来维持收集器工作。

对象援用关系扭转

如何解决用户线程扭转对象援用关系?

之前说过,G1 收集器则是通过原始快照(SATB)算法来实现的。

垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存调配上,程序要持续运行就必定会继续有新对象被创立,G1 为每一个 Region 设计了两个名为 TAMS(Top at Mark Start) 的指针,把 Region 中的一部分空间划分进去用于并发回收过程中的新对象调配,并发回收时新调配的对象地址都必须要在这两个指针地位以上。G1 收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范畴。与 CMS 中的“Concurrent Mode Failure”失败会导致 Full GC 相似,如果内存回收的速度赶不上内存调配的速度,G1 收集器也要被迫解冻用户线程执行,导致 Full GC 而产生长时间“Stop The World”。

用户通过 -XX:MaxGCPauseMillis 参数指定的进展工夫只意味着垃圾收集产生之前的期望值。在垃圾收集过程中,G1 收集器会记录每个 Region 的回收耗时、每个 Region 记忆集里的脏卡数量等各个可测量的步骤破费的老本,并剖析得出平均值、标准偏差、置信度等统计信息。而后通过这些信息预测当初开始回收的话,由哪些 Region 组成回收集才能够在不超过冀望进展工夫的束缚下取得最高的收益。

运作过程

  1. 初始标记(Initial Marking):仅仅只是标记一下 GC Roots 能间接关联到的对象,并且批改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中调配新对象。这个阶段须要进展线程,但耗时很短,而且是借用进行 Minor GC 的时候同步实现的,所以 G1 收集器在这个阶段理论并没有额定的进展。
  2. 并发标记(Concurrent Marking):从 GC Root 开始对堆中对象进行可达性剖析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描实现当前,还要重新处理 SATB 记录下的在并发时有援用变动的对象。
  3. 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于解决并发阶段完结后仍遗留下来的最初那大量的 SATB 记录。
  4. 筛选回收(Live Data Counting and Evacuation):负责更新 Region 的统计数据,对各个 Region 的回收价值和老本进行排序,依据用户所冀望的进展工夫来制订回收打算,能够自由选择任意多个 Region 形成回收集,而后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全副空间。这里的操作波及存活对象的挪动,是必须暂停用户线程,由多条收集器线程并行实现的。
  • 从上述阶段的形容能够看出,G1 收集器除了并发标记外,其余阶段也是要齐全暂停用户线程的
  • G1 从整体来看是基于“标记 - 整顿”算法实现的收集器,但从部分(两个 Region 之间)上看又是基于“标记 - 复制”算法实现

G1 默认的进展指标为两百毫秒,但如果咱们把进展工夫调得非常低,譬如设置为二十毫秒,很可能呈现的后果就是因为进展指标工夫太短,导致每次选出来的回收集只占堆内存很小的一部分,收集器收集的速度逐步跟不上分配器调配的速度,导致垃圾缓缓沉积。利用运行工夫一长,最终占满堆引发 Full GC 反而升高性能,所以通常把冀望进展工夫设置为一两百毫秒或者两三百毫秒会是比拟正当的。

CMS VS G1

相比 CMS,G1 的长处有很多,较为显著的长处就是 G1 不会产生垃圾碎片。不过,G1 绝对于 CMS 依然不是占全方位、压倒性劣势的,至多 G1 无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额定执行负载(Overload)都要比 CMS 要高。

就内存占用来说,尽管 G1 和 CMS 都应用卡表来解决跨代指针,但 G1 的每个 Region 都必须有一份卡表,这导致 G1 的记忆集可能会占整个堆容量的 20% 乃至更多的内存空间;相比起来 CMS 的卡表就相当简略,全局只有一份。

在执行负载的角度上,譬如它们都应用到写屏障,CMS 用写后屏障来更新保护卡表;而 G1 除了应用写后屏障来进行同样的卡表保护操作外,为了实现原始快照搜寻(SATB)算法,还须要应用写前屏障来跟踪并发时的指针变动状况 。相比起增量更新算法,原始快照搜寻可能缩小并发标记和从新标记阶段的耗费,防止 CMS 那样在最终标记阶段进展工夫过长的毛病,然而在用户程序运行过程中的确会产生由跟踪援用变动带来的额外负担。 因为 G1 对写屏障的简单操作要比 CMS 耗费更多的运算资源,所以 CMS 的写屏障实现是间接的同步操作,而 G1 就不得不将其实现为相似于音讯队列的构造,把写前屏障和写后屏障中要做的事件都放到队列里,而后再异步解决 。目前在小内存利用上 CMS 的体现大概率依然要会优于 G1,而在大内存利用上 G1 则大多能施展其劣势, 这个优劣势的 Java 堆容量平衡点通常在 6GB 至 8GB 之间。

相干参数

参数形容
-XX:+UseG1GC手动指定应用 G1 收集器执行内存回收工作(JDK9 后不必设置,默认就是 G1)
-XX:G1HeapRegionSize设置每个 Region 的大小。值是 2 的幂, 范畴是 1MB 到 32MB 之间,指标是依据最小的 Java 堆大小划分出约 2048 个区域。默认是堆内存的 1 /2000
-XX:MaxGCPauseMillis设置冀望达到的最大 GC 进展工夫指标
-XX:InitiatingHeapOccupancyPercent简称为 IHOP,设置触发并发 GC 周期的 Java 堆占用率阈值。超过此值,就触发 GC。默认值是 45%
-XX:+G1UseAdaptiveIHOP主动调整 IHOP 的指,JDK9 之后可用
-XX:GCTimeRatio这个参数为 0~100 之间的整数(G1 默认是 9),值为 n 则零碎将破费不超过 1/(1+n) 的工夫用于垃圾收集。因而 G1 默认最多 10% 的工夫用于垃圾收集

如果本篇博客有任何谬误和倡议,欢送给我留言斧正。文章继续更新,能够关注公众号第一工夫浏览。

正文完
 0