深刻了解 JVM – G1 收集器
前言
上一篇通过案例阐明了老年代的常见优化和解决形式,这一节来看下目前最为热门的 G1 收集器,G1 收集器也是 JDK9 服务端默认的垃圾收集器,尽管 JDK9 在当初看来还不是非常的遍及,然而学习这个垃圾收集器是非常重要也是十分必要的。
前文回顾
上一节咱们通过一个电商的模仿实战,理解了老年代常见的优化形式。同时在最初总结了老年代优化的一些常见的套路,上面间接用上一节的总结回顾整个内容:
- 首先业务的对象都是生命周期非常短暂的对象,新生代的压力比老年代要大,所以适当放大老年代空间是非常划算的
- 预测在高并发的场景下对象进入老年代的机会,如果对象常常“跨区”阐明有一部分内容空间是节约了,那就是 Survior 区域
- 对象在各分区须要大抵多少的内存空间,比方每个线程须要占用多少的内存空间
- 对象的年龄判断是否须要改变,提前让对象进入老年代是益处还是害处
- 关注收集器对于对象垃圾回收的影响,同时在启动的时候要强制应用某一垃圾收集器,因为不同的 JDK 版本默认的垃圾收集器是不一样的。
CMS+ParNew 收集器的痛点是什么?
在之前的文章咱们提到过,传统的黄金组合的痛点 Stop world(世界进展):在 并发标记和并发整顿 阶段如果呈现调配对象超过老年代代而导致 Full Gc,会立即停下手头的所有工作全力进行垃圾收集的动作,并且应用 Serial old 收集器解决,这对于用户来说会发现显著的卡顿,同时会误认为零碎卡死,体验非常差。
G1 收集器的介绍
为什么会诞生 G1?
感兴趣能够看下对于 G1 论文的白皮书:http://citeseerx.ist.psu.edu/…。(看评论说其实讲的非常形象和抽象,倡议深入研究 G1 的大抵看看)
为什么会诞生 G1 呢?最大起因是JVM 目前不能让收集器的垃圾回收进展在某个特定工夫范畴,然而也不须要像实时那样要求非常的严格。基于这样的思考,早在 06 年就呈现了 G1 的论文,然而最终实现花了好多年的工夫!能够设想这个收集器外部注定是非常复杂的,当然咱们没有必要去深入研究底层,只须要他的根底原理即可。
历史进程:
- Jdk7 update4 被商用
- jdk8 update40 并发类型卸载 反对
- Jdk9 成为服务端默认垃圾收集器
- Jdk10 因为 cms 垃圾收集器插件性能耦合等缘故,重构了“对立垃圾回收接口”
G1 收集器的特点
- 设置一个 垃圾的预期进展工夫。依据 Region 的大小和回收价值进行最有效率的回收。
- 内存不再固定划分新生代和老年代,应用 Region 对于内存进行分块,实现了依据系统资源动静分代。
- Region 可能属于新生代或者老年代,同时调配给新生代还是老年代是由 G1 本人管制的。
- 抉择最小回收工夫以及最多回收对象的 region 进行垃圾的回收操作。
补充《深刻了解 JVM 虚拟机》介绍的特点:
- 不再保持固定大小以及固定数量的分区划分
- 应用 region 划分区域,大小相等,不同区域表演不同的角色
- humongous 区域:设计被用于大对象应用,依据最短进展工夫模型优先回收价值最高的 region。超过一个 region 一半的大小都为大对象
- 后盾应用一个优先级列表优先回收收益最大的区域
G1 的 Region
G1 勾销了固定分代的概念,取而代之的是应用分区的概念,把内存切分成一个个小块,同时各个小块又分为新生代(eden、Survior 区)、老年代、大对象(humongous 区)等等。
G1 规定大于 Region 的一半的对象成为大对象,同时 不参加分代。
Region 并不是固定为新生代或者老年代,通常状况为自在状态,只有在须要的时候会被划分为指定的分代并且寄存特定对象。这里兴许会有疑难,这样调配 Region 不会产生很多垃圾碎片么?答案当然是并不会,首先新生代应用复制算法,会把存活对象拷贝到一块 region 进行寄存,同时存活对象大于肯定的 region 占比不会进行复制(下文会提到),把整个 region 清理并且进行回收,而老年代则应用标记 - 整顿算法,同样的情理也会间接把垃圾对象的 region 给清理掉,也是不会产生内存碎片的,最初,大对象是横跨多个 region 并由专门的 region 寄存,一旦回收间接干掉大对象把对应的 region 清理即可。
到底有多少 region,每个 region 的大小是多少?
计算形式: 堆大小 / 2048,G1 收集器最多能够有 2048 个 region,并且大小必须是 2 的倍数。也就是说 4G 内存给每一个 region 的大小是 2M 的大小。
上面是 G1 常见的配置参数:
-XX:Heap Region Size:手动制订 region 的大小。单个 region 的大小指定,默认为 2M,4g 内存当中
-XX:G1NewSizePercent:手动指定新生代初始占比,默认是 5%
-XX:G1MaxNewSizePercent:新生代的最大占比默认是 60%
罕用参数:
-XX:G1MixedGCCountTarget
:示意一次垃圾回收之后,最初一次混合回收执行几次。这个参数意味着在垃圾回收的最初一个阶段回收和零碎运行 交替运行,并且默认回收 8 次之后完结这个操作。-XX:G1HeapWastePercent
:默认为 5%。在混合回收的时候如果一次混合回收的 闲暇 region 超过 5%就立即进行垃圾回收的操作,这个参数也是为了尽量减少进展的工夫设计的。-XX:G1MixedGCLiveThreasholdPercent
:默认值为 85%,示意存活对象要低于 85% 才进行回收的操作。因为如果一个 region 的存活对象大于 85%,复制拷贝的代价是非常大的,并且这一类 region 大概率进入老年代-XX:G1MaxNewSizePercent
:新生代最大占用堆内存空间,默认值为 60%。这个参数意味着在初始 5% 的新生代状况下,零碎运行最多能够从零碎总空间调配 60% 给新生代应用,超过这个值就必定会触发新生代回收。-XX:NewRatio=n
:配置新生代与老年代的比例,默认是 2:1
如何了解 G1 的工作模式?
为了更好了解 G1 的工作模式进展工夫模型,这里集体想了一个还算活泼的例子解释下 G1 的工作模式:
咱们平时去服务比拟周到的餐馆,服务员通常会看咱们桌子上垃圾的量有多少或者吃剩的盘子有多少,当垃圾超过一定量之后,服务员就会过去开盘子或者收垃圾,而后不便前面上菜,同时也能够让洗盘子的服务员忙忙停停的工作。
这之后就会思考,如果每多出一个盘子就去收一个,你会不会很累,会不会等到盘子有一定量了再去收,比方桌子摆不下新的菜了,或者盘子叠了很多个。
最初,如果每次开盘子的速度能赶上上菜的速度,那么根本的失常运行是没有大问题的。
G1 适宜什么样的零碎?
G1 适宜的零碎包含 超大内存 的零碎,此时如果依照传统的分代,比方 16G 的内存新生代 8G,老年代 8G 这种划分,尽管拉长垃圾进展的工夫,然而一旦新生代被占满,回收的效率是非常低的,因为对象和 GC ROOT 十分多,最终导致垃圾收集长时间卡顿。而 G1 不一样,他只须要依照算法判断依据进展模型的值在新生代靠近进展模式工夫的时候就马上开启回收,不必等新生代满了才回收。
G1 也适宜 须要低提早 的零碎,因为低提早对于零碎的响应要求是十分高的,更加看重响应的工夫,同时对于零碎的资源要求也比拟高,而分代的模型和实践在大内存机器会造成长时间的垃圾收集进展,这对于实时响应的服务要求是十分高的。
G1 比照 Parnew+cms 最大的提高在哪里?
G1 最大的提高也是他的特点就是 工夫进展模型,能够管制 stop world 的工夫。同时能够让垃圾收集的工夫管制在预期设置的范畴之内。
其余提高:
- 算法:G1 基于标记 - 整顿算法, 不会产生空间碎片,调配大对象时不会无奈失去间断的空间而提前触发一次 FULL GC。
- 多线程:多线程算法比 CMS 更加优良,同时更能施展多核性能
G1 没有毛病了?
没毛病是不可能的,必定是有毛病,这个收集器最大的问题恰好在于进展模型,外面的算法细节非常的简单,所以咱们调试 不能仅仅通过业务推算,而是要依据日志以及工具辅助来实现调优,G1 的调优是十分麻烦的一件事。
其次就是 Region 的设计了,region 的设计自身要耗费大量的系统资源进行保护的,这意味着 G1 收集器自身至多须要占用 10%-20% 的内存来维持本人的失常工作,比方计算进展模型,维持跨代援用和 GC ROOT 等信息,这两头蕴藏的细节十分复杂,这里的内容能够参考【其余材料 - 干货文章】这部分理解,所以 G1 收集器自身就须要较好的机器性能才倡议应用。
上面是依据《深刻了解 JVM 虚拟机》书中总结 G1 的毛病:
- 每个 region 要更大的内存空间解决记忆集的耗费问题,还须要 tams 耗费的局部内存实现快照的性能。
- cms 应用写后屏障,而 G1 不仅应用写后屏障还用了写前屏障。额定应用队列进行异步并发标记的对象指针改变的问题(最终标记的工夫开销)
- 因为 cms 应用卡集的形式增量更新,只须要写屏障的卡表援用,但会导致用户线程暂停.
G1 的垃圾回收步骤
初始标记:
和 cms 垃圾收集器相似,在初始的的状况须要 stop the world 的操作。仅仅标记 GC ROOT 能够援用的对象,整个过程十分快。
这里能够看到对空间的构造曾经和传统固定的分代模型曾经不一样了,堆内存被划分为小块的 region,初始标记会依据栈中的局部变量援用或者传递援用等标记初始的 GC ROOT 对象,这个过程能够比拟快的实现,因为仅仅是简略标记而已。
并发标记:
这个阶段也和 cms 比拟相似,同样能够和用户线程一起 并发运行,零碎能够失常调配对象,而垃圾回收线程依据 GC ROOT 进行对象的援用,标记存活对象。同时将对象的改变进行标记记录,这个阶段会比拟消耗系统资源,然而和零碎线程一起并发影响也不是很大。
最终标记:
最终标记阶段:这个阶段和 CMS 相似,也是须要 stop world,此时零碎过程须要暂停,进行对象的调配,同时垃圾收集器负责对于对象进行最初的标记和分类动作,决定哪些对象须要被垃圾回收。
筛选回收:
筛选回收阶段:须要重点记忆的一个阶段。这个阶段会计算老年代有多少个 region 存活,存活对象的占比,以及回收的效率计算。
这个阶段也是须要 stop world,会让垃圾收集线程马力全开,在指定的进展工夫范畴内实现更多的垃圾回收动作。同时这个阶段会反复屡次,并且在回收的时候和零碎线程是交替运行的,也就是说回收的时候须要暂停。
另外回收阶段不仅回收老年代,还回收新生代以及大对象,算是真正意义上的 full GC。比方:老年代有 1000 个 region,而通过计算发现须要回收的 region 为 800 个,那么就会回收 800 个 REGION
这里再联合步骤阐明一下下面的局部参数的作用,从上图能够看到,零碎线程和垃圾回收是交替运行的,在最初一步默认会让零碎和垃圾收集线程交互运行 8 次,假如进展工夫设置为 200ms,那么就是每次在 200ms 内尽可能回收垃圾,回收实现立马开启零碎线程,而后运行一段时间又进行回收,如此重复,如果在垃圾回收中 4 次就回收掉超过 5%,那么意味着这一个阶段提前完成了,就会立马进入下一次的垃圾回收循环。
对应回收参数:
-XX:G1MixedGCCountTarget
:示意一次垃圾回收之后,最初一次混合回收执行几次。这个参数意味着在垃圾回收的最初一个阶段回收和零碎运行交替运行,并且回收 8 次之后完结这个操作。-XX:G1HeapWastePercent
:默认为 5%。在混合回收的时候如果一次混合回收的闲暇 region 超过 5% 就立即进行垃圾回收的操作,这个参数也是为了尽量减少进展的工夫设计的。
G1 的 FAQ
G1 在什么时候会触发 Mixed GC
参数:-XX:InterfaceTestControllernitiatingHeapOccupancyPercent
,他的默认值是 45%。
意思就是说如果老年代占用超过了 45%,就会触发一个叫做混合回收的操作 , 混合回收意味着新生代和老年代一起回收,这时候毫无疑问整个零碎线程都会进行。
回收失败了如何解决?
回收失败了,就会进行线程,而后通过单线程的 serrial 形式对于所有 region 存活对象标记,整顿,而后清理垃圾对象,整个过程是十分迟缓的。
这个过程其实和 CMS 的 Corrurnet mode fail 相似,然而因为 G1 的内存模型齐全另辟蹊径,所以他须要回收新生代,老年代和大对象,算法的细节要更为简单,整顿复原的工夫也比拟长,也能够说 G1 的回收是真正意义上的 Full GC。
G1 还存在 eden 区域和 survior 区域么?
答案是尽管不须要指定新生代和老年代的大小由 G1 管制,然而 region 自身在运行时还是会划分新生代或者老年代,只是不再是固定的了,所以新生代还是有对应的 Eden 和 Survior 区域。
G1 的新生代是如何回收的?
还是采纳 复制算法 ,当新生代超过默认的 60% 的最大占比限度的时候,会触发Minor gc,而后进行stop world 的操作。
这里可能会想这不还是和之前没区别么?之前说过 G1 的特点是指定垃圾回收的最大进展工夫。G1 会依据 Region 的大小和回收预测工夫在咱们指定的最大回收工夫内尽可能的回收内存,回收之后的内存能够给新生代应用也能够给老年代应用。
G1 的老年代是如何回收的?
这里须要留神,G1 自身曾经没有老年代回收这个概念了,取而代之的是 Mixed Gc 也就是混合回收,当老年代的 region 占比超过 45% 就会触发。
G1 的回收和之前分代的垃圾收集器有什么区别?
首先须要弄清一个概念,就是 region 尽管是分代存储的然而并不代表始终是分代的 region,比方新生代如果有 600 个 region,回收了 200 个 region,这 200 个 region 等于是“自在”的,能够分给新生代也能够给老年代应用。
G1 的进展工夫模型会依据用户设置的比方 200MS 内进行回收操作,能够通过:-XX:MaxGCPauseMills 这个参数设置最大的进展等待时间,g1 会依据此参数追踪最有回收价值的 region 进行解决,然而这个参数其实是一个软指标,并不是说收集器齐全保障这个时间段内实现回收,而是意味着在这个时间段内做 最有价值的回收,就像前文提到的开盘子的概念一样,他会尽可能的解决让垃圾收集速度更上调配的速度。
对象什么时候会进入老年代?
- 对象在新生代躲过了很屡次垃圾回收,达到肯定的对象年龄,-XX:MaxTenuringThreashold这个参数能够设置年龄。
- 依据 Survior 总体存活率超过 50% 的状况判断,当排序发现某一年龄对象大小超过 survior 区域的 50% 会触发。
大对象什么时候回收?
大对象不属于新生代也不属于老年代,所以在新生代或者老年代回收的时候会顺带回收大对象的 region 内存。也就是说大对象的回收依附 Mixed 回收。
其余材料:
算法细节摘录
本人看书的一些笔记,不适宜作为注释,所以放到最初(反正也没人看,哈哈)
Region 应用两个 ams 指针把 region 的一部分空间划分对象调配应用,所有新调配的对象存在此区间,同样如果回收赶不上内存调配,也要解冻用户线程,进行 stop world。
可预测模型依附:-XX:MaxGcRauseMillion 期望值,G1 会在期望值内做出最有效率的回收
算法:
G1 应用 衰减均值算法,垃圾回收计算 region 回收耗时和脏卡数据
- 参照值:平均值,标准偏差,置信度信息
- 默认 200 毫秒进展工夫,低于这个值很可能垃圾回收速度赶不上调配对象速度
干货文章:
知乎:G1 收集器原理了解与剖析
求教 G1 算法的原理
美团:Java Hotspot G1 GC 的一些关键技术
写在最初:
最初吐槽一句,JVM 真的很难,光看《深刻了解 JVM 虚拟机》这本书也只能大略理解目前支流收集器实现的大抵原理,如果要深究须要长时间的积攒,当然咱们不须要学的那么苦楚,这篇文章讲到的内容根本能应酬 80、90% 的场景了。
下一篇文章依据一个案例讲一下 G1 的大抵优化思路,留神只是大抵思路,和之前的分代收集器不同,G1 要真刀真枪的去跑工具,看日志,做数据分析能力调优好,因为能用上 G1 收集器的零碎少数不会小,小零碎用用分代并且并发访问量不大的也不须要怎么调优。