本文已收录至 Github,举荐浏览 👉 Java 随想录
微信公众号:Java 随想录
CSDN:码农 BookSea
转载请在文首注明出处,如发现歹意剽窃 / 搬运,会动用法律武器保护本人的权利。让咱们一起保护一个良好的技术创作环境!
CMS 意志的继承者—G1
题目叫做 CMS 意志的继承者,听起来有那么点中二,然而我感觉来形容 G1 正合适。G1 一开始被赋予的冀望是用来替换 CMS 的。JDK 9 公布之日,G1 成为服务端模式下的默认垃圾收集器,而 CMS 则沦落至被申明为不举荐应用(Deprecate)的收集器。如果对 JDK 9 及以上版本的 HotSpot 虚拟机应用 CMS 收集器的话,用户会收到一个正告信息,提醒 CMS 将来将会被废除。能够说 G1 是在 CMS 的根底上应运而生的,继承了 CMS 的意志和使命。
Garbage First(简称 G1)收集器是垃圾收集器技术倒退历史上的里程碑式的成绩,它创始了收集器面向部分收集的设计思路和基于 Region 的内存布局模式。G1 的设计相当的简单,设计者们设计 G1 的时候心愿 G1 可能建设起“进展工夫模型”
,进展工夫模型的意思是可能反对指定在一个长度为 M 毫秒的工夫片段内,耗费在垃圾收集上的工夫大概率不超过 N 毫秒这样的指标。下文会有所讲述。
基于 Region 的堆内存布局
首先,介绍下 G1 基于 Region 的堆内存布局,这是可能可能建设起 “进展工夫模型”
的要害。
G1 逻辑上分代,然而物理上不分代。
G1 不再保持固定大小以及固定数量的分代区域划分,而是把间断的 Java 堆划分为多个大小相等的独立区域(Region),每一个 Region 都能够依据须要,表演新生代的 Eden 空间、Survivor 空间,或者老年代空间。收集器可能对表演不同角色的 Region 采纳不同的策略去解决。
这样就不存在界线,无论是新创建的对象还是曾经存活了一段时间、熬过屡次收集的旧对象都能获取很好的收集成果。
Region 中还有一类非凡的 Humongous 区域,专门用来存储大对象。G1 认为只有大小超过了一个 Region 容量一半的对象即可断定为大对象。 每个 Region 的大小能够通过参数 -XX:G1HeapRegionSize
设定,取值范畴为 1MB~32MB,且应为 2 的 N 次幂。而对于那些超过了整个 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 组成回收集才能够在不超过冀望进展工夫的束缚下取得最高的收益。
运作过程
- 初始标记(Initial Marking):仅仅只是标记一下 GC Roots 能间接关联到的对象,并且批改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中调配新对象。这个阶段须要进展线程,但耗时很短,而且是借用进行 Minor GC 的时候同步实现的,所以 G1 收集器在这个阶段理论并没有额定的进展。
- 并发标记(Concurrent Marking):从 GC Root 开始对堆中对象进行可达性剖析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描实现当前,还要重新处理 SATB 记录下的在并发时有援用变动的对象。
- 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于解决并发阶段完结后仍遗留下来的最初那大量的 SATB 记录。
- 筛选回收(Live Data Counting and Evacuation):负责更新 Region 的统计数据,对各个 Region 的回收价值和老本进行排序,依据用户所冀望的进展工夫来制订回收打算,能够自由选择任意多个 Region 形成回收集,而后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全副空间。这里的操作波及存活对象的挪动,是必须暂停用户线程,由多条收集器线程并行实现的。
从上述阶段的形容能够看出,G1 收集器除了并发标记外,其余阶段也是要齐全暂停用户线程的,换言之,它并非纯正地谋求低提早,官网给它设定的指标是在提早可控的状况下取得尽可能高的吞吐量,所以能力担当起“全功能收集器”的重任与冀望。
毫无疑问,能够由用户指定冀望的进展工夫是 G1 收集器很弱小的一个性能,不过不要胡思乱想,毕竟 G1 是要解冻用户线程来复制对象的,这个进展工夫再怎么低也得有个限度。它默认的进展指标为两百毫秒 ,一般来说,回收阶段占到几十到一百甚至靠近两百毫秒都很失常,但如果咱们把进展工夫调得非常低,譬如设置为二十毫秒,很可能呈现的后果就是因为进展指标工夫太短,导致每次选出来的回收集只占堆内存很小的一部分, 收集器收集的速度逐步跟不上分配器调配的速度,导致垃圾缓缓沉积。很可能一开始收集器还能从闲暇的堆内存中取得一些喘息的工夫,但利用运行工夫一长就不行了,最终占满堆引发 Full GC 反而升高性能,所以通常把冀望进展工夫设置为一两百毫秒或者两三百毫秒会是比拟正当的。
相比 CMS,G1 的长处有很多,暂且不管能够指定最大进展工夫、分 Region 的内存布局、按收益动静确定回收集这些创新性设计带来的红利,单从最传统的算法实践上看,G1 也更有发展潜力。与 CMS 的“标记 - 革除”算法不同,G1 从整体来看是基于“标记 - 整顿”算法实现的收集器,但从部分(两个 Region 之间)上看又是基于“标记 - 复制”算法实现,无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,垃圾收集实现之后能提供规整的可用内存。这种个性有利于程序长时间运行,在程序为大对象分配内存时不容易因无奈找到间断内存空间而提前触发下一次收集。
不过,G1 绝对于 CMS 依然不是占全方位、压倒性劣势的,至多 G1 无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额定执行负载(Overload)都要比 CMS 要高。
就内存占用来说,尽管 G1 和 CMS 都应用卡表来解决跨代指针,但 G1 的卡表实现更为简单,而且堆中每个 Region,无论表演的是新生代还是老年代角色,都必须有一份卡表,这导致 G1 的记忆集(和其余内存耗费)可能会占整个堆容量的 20% 乃至更多的内存空间;相比起来 CMS 的卡表就相当简略,只有惟一一份,而且只须要解决老年代到新生代的援用,反过来则不须要,因为新生代的对象具备朝生夕灭的不稳定性,援用变动频繁,能省下这个区域的保护开销是很划算的。
在执行负载的角度上,譬如它们都应用到写屏障,CMS 用写后屏障来更新保护卡表;而 G1 除了应用写后屏障来进行同样的(因为 G1 的卡表结构复杂,其实是更繁缛的)卡表保护操作外,为了实现原始快照搜寻(SATB)算法,还须要应用写前屏障来跟踪并发时的指针变动状况 。相比起增量更新算法,原始快照搜寻可能缩小并发标记和从新标记阶段的耗费,防止 CMS 那样在最终标记阶段进展工夫过长的毛病,然而在用户程序运行过程中的确会产生由跟踪援用变动带来的额外负担。 因为 G1 对写屏障的简单操作要比 CMS 耗费更多的运算资源,所以 CMS 的写屏障实现是间接的同步操作,而 G1 就不得不将其实现为相似于音讯队列的构造,把写前屏障和写后屏障中要做的事件都放到队列里,而后再异步解决 。目前在小内存利用上 CMS 的体现大概率依然要会优于 G1,而在大内存利用上 G1 则大多能施展其劣势, 这个优劣势的 Java 堆容量平衡点通常在 6GB 至 8GB 之间,当然,以上这些也仅是经验之谈,不同利用须要因地制宜地理论测试能力得出最合适的论断,随着 HotSpot 的开发者对 G1 的一直优化,也会让比照后果持续向 G1 歪斜。