本文已收录至GitHub,举荐浏览 Java随想录
微信公众号:Java随想录
原创不易,重视版权。转载请注明原作者和原文链接
上篇文章咱们聊了CMS,这篇就来好好唠唠G1。
CMS和G1能够说是一对欢喜冤家,面试问你CMS,总喜爱把G1拿进来进行比拟。
G1在JDK7中退出JVM,在JDK9中成为了默认的垃圾收集器,如果在JDK8中应用G1,咱们能够应用参数 -XX:+UseG1GC 来开启。
G1和CMS相比有哪些优缺点?G1为什么可能建设可进展的工夫模型?
别着急,本篇文章通知你答案。
G1,全名叫:Garbage First。是垃圾收集器技术倒退历史上的里程碑式的成绩,创始了收集器面向部分收集的设计思路和基于Region的内存布局模式。
这句话啥意思?
在G1之前的垃圾回收器,如Parallel Scavenge、Parallel Old、CMS等,次要针对Java堆内存中的特定局部(新生代或老年代)进行操作。然而,G1将Java堆划分为多个「小区域」,并依据每个区域中垃圾对象的数量和大小来优先进行垃圾回收。
称之为「基于Region的内存布局」。
另外设计者们设计G1的时候心愿G1可能建设起「进展工夫模型」,进展工夫模型的意思是可能反对指定在一个长度为M毫秒的工夫片段内,耗费在垃圾收集上的工夫大概率不超过N毫秒这样的指标。
所以咱们可能总结出G1身上的两个标签:
- 基于Region的内存布局
- 进展工夫模型
先来说说基于内存布局是怎么个事儿。
基于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作为老年代的一部分来进行对待。
调配大对象的时候,因为占用空间太大,可能会过早产生GC进展。G1在每次调配大对象的时候都会去查看以后堆内存占用是否超过初始堆占用阈值IHOP(The Initiating Heap Occupancy Percent),缺省状况是Java堆内存的45%。当老年代的空间超过45%,G1会启动一次混合周期。
可预测的进展工夫模型
基于Region的进展工夫模型是G1可能建设可预测的进展工夫模型的前提。
G1将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样能够有打算地防止在整个Java堆中进行全区域的垃圾收集。
G1收集器会去跟踪各个Region外面的垃圾沉积的「价值」大小,价值即回收所取得的空间大小以及回收所需工夫的经验值,而后在后盾保护一个优先级列表。
每次依据用户设定容许的收集进展工夫(应用参数-XX:MaxGCPauseMillis
指定,默认值是200毫秒),优先解决回收价值收益最大的那些Region,这也就是「Garbage First」名字的由来。
这种应用Region划分内存空间,以及具备优先级的区域回收形式,保障了G1收集器在无限的工夫内获取尽可能高的收集效率。
所以说G1实现可预测的进展工夫模型的要害就是Region布局
和优先级队列
。看起来如同G1的实现也不简单,然而其实有许多细节是须要思考的。
跨Region援用对象
首先第一个问题:G1将Java堆分成多个独立Region后,Region外面存在的跨Region援用对象如何解决?
实质上还是咱们之前提过的「跨代援用」问题,解决方案的思路咱们曾经晓得,应用「记忆集」。
G1的记忆集在存储构造的实质上是一种「哈希表」,Key是别的Region的起始地址,Value是一个汇合,外面存储的元素是卡表的索引号。
应用记忆集诚然没啥故障,然而麻烦的是,G1的堆内存是以Region为根本回收单位的,所以它的每个Region都保护有本人的记忆集,这些记忆集会记录下别的Region指向本人的指针,并标记这些指针别离在哪些卡页的范畴之内。
因为Region数量较多,每个Region都保护有本人的记忆集,光是存储记忆集这块就要占用相当一部分内存,G1比其余圾收集器有着更高的内存占用累赘。依据教训,G1至多要消耗大概相当于Java堆容量10%至20%的额定内存来维持收集器工作。
这能够说是G1的缺点之一。
除了跨代援用外,对象援用关系扭转,如何解决?
对象援用关系扭转
解决的方法咱们之前在讲「三色标记算法」的时候提过,G1应用「原始快照」来解决这一问题。
垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存调配上,程序要持续运行就必定会继续有新对象被创立。
G1为每一个Region设计了两个名为「TAMS(Top at Mark Start)」的指针。
把Region中的一部分空间划分进去用于并发回收过程中的新对象调配,并发回收时新调配的对象地址都必须要在这两个指针地位以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范畴。
与CMS中的「Concurrent Mode Failure」失败会导致Full GC相似,如果内存回收的速度赶不上内存调配的速度,G1收集器也要被迫解冻用户线程执行,导致Full GC而产生长时间Stop The World。
G1能够通过-XX:MaxGCPauseMillis
参数设置垃圾收集的最大进展工夫的JVM参数,单位为毫秒。
在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤破费的老本,并剖析得出平均值、标准偏差、置信度等统计信息。
而后通过这些信息预测当初开始回收的话,由哪些Region组成回收集才能够在不超过冀望进展工夫的束缚下取得最高的收益。
G1收集器会依据这个设定值进行自我调整以尽量达到这个暂停工夫指标。例如,如果设定了-XX:MaxGCPauseMillis=200
,那么JVM会尽力保障大部分(但并非全副)的GC暂停工夫不会超过200毫秒。
运作过程
- 初始标记(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在逻辑上依然采纳了分代的思维,从整体来看是基于「标记-整顿」算法实现的收集器,但从部分(两个Region之间)上看又是基于「标记-复制」算法实现。
这时候有些点子王可能会想,如果我把-XX:MaxGCPauseMillis
,调的十分小,那是不是就回收的更快了?
G1默认的进展指标为两百毫秒,但如果咱们把进展工夫调得非常低,譬如设置为二十毫秒,很可能呈现的后果就是因为进展指标工夫太短,导致每次选出来的回收集只占堆内存很小的一部分,收集器收集的速度逐步跟不上分配器调配的速度,导致垃圾缓缓沉积。
利用运行工夫一长,最终占满堆引发Full GC反而升高性能,所以通常把冀望进展工夫设置为一两百毫秒或者两三百毫秒会是比拟正当的。
CMS VS G1
相比CMS,G1的长处有很多,较为显著的长处就是G1不会产生垃圾碎片。
But,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之间。
文章的最初放一组G1的罕用参数:
参数 | 形容 |
---|---|
-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% 的工夫用于垃圾收集 |
最初吐槽一句,JVM真的很难,垃圾收集器的外部原理切实太简单,如果要深究须要长时间的积攒。
当然咱们不是JVM的业余人员,不须要学的那么深刻,这篇文章讲到的内容能根本应酬面试和工作场景了。
那本篇文章到这完结啦,咱们下篇再见。
感激浏览,如果本篇文章有任何谬误和倡议,欢送给我留言斧正。
老铁们,关注我的微信公众号「Java 随想录」,专一分享Java技术干货,文章继续更新,能够关注公众号第一工夫浏览。
一起交流学习,期待与你共同进步!