jdk1.8 垃圾收集器_jdk8可以使用G1收集器吗

(93) 2024-08-06 19:01:01

原文在这里:https://docs.oracle.com/en/java/javase/11/gctuning/garbage-first-garbage-collector.html

同时欢迎观看本人录得两个视频教程:

  1. Java生产环境下性能监控与调优详解 里面还有很多监控调优的手段
  2. Java秒杀系统方案优化 高性能高并发实战

转载请标明出处:JDK11-G1垃圾收集器

G1垃圾收集器简介

G1垃圾收集器主要是为那些拥有大内存的多核处理器而设计的。它在以很高的概率满足垃圾收集的停顿时间的要求同时还可以达到很高的吞吐量,同时几乎不需要做什么配置。G1的目标是为应用提供停顿时间和吞吐量的最佳平衡,它的主要特性包含:

  • 堆内存达到数十个G甚至更大,超过50%的堆内存都是存活的对象
  • 对象分配和晋升的速度随时间变化非常大
  • 堆中存在大量的内存碎片
  • 可预测的停顿时间的目标在几百毫秒以内,不会存在长时间的停顿

在jdk11中,G1已经取代了CMS,是默认的垃圾收集器

在随后的章节会介绍G1收集器达到如此高性能和满足停顿时间目标的多种方式。

转载请标明出处:JDK11-G1垃圾收集器

启用G1

因为G1是默认的收集器,因此一般不需要做任何额外的操作就开启。你也可以用 -XX:+UseG1GC来明确开启(译者注:因为是JDK11)。

基础的一些概念

G1是一款分带、增量、并行、大部分时候并发、STW并且标记整理的收集器,在每一个STW停顿的时候,它都会监控停顿时间的目标。跟其他的收集器类似,G1把堆分成逻辑上的young区和old区。内存回收主要集中在young区,在这个区域的内存回收也是非常高效的,偶尔也会发生在old区。

为了提高吞吐量,有些操作总是STW的,还有一些操作在应用停止的情况下会花费更多的时间,比如一些对整个堆的操作,像全局的标记就是并行和并发来执行的。在空间回收的时候,为了让STW时间更短,G1是增量的分步和并行来回收的。G1是通过记录上一次应用的行为和GC的停顿信息来实现可预计的停顿时间的,可以利用这些信息来计算在停顿时间之内要做的工作的多少。比如:G1会首先回收那些可以高效回收的内存区域(也就是大部分都被填满垃圾的区域,这也是为啥叫G1的原因)。

G1大部分使用标记整理算法来回收内存:收集选中的内存区域中的存活对象,然后把他们拷贝到新的内存区域中,同时会对这些对象占用的空间进行压缩。回收完成以后,之前被存活对象占用的空间可以被应用用来重新分配对象。

G1并不是一个实时的垃圾收集器。长时间来看,它可以以很高的概率满足停顿时间的要求,但并不能绝对满足。

堆的结构

G1把整个堆分成很多相等大小的块(Region),每一个region都是一些连续的虚拟内存就如同图9-1所示。region是内存分配和回收的基本单元,在某个特定的时间点,一个region可能是空的(浅灰色表示),或者是分配到了某个区(generation)中,可能是young区或者old区。当收到需要分配内存的请求的时候,内存管理器就会提供空闲的region出去。内存管理器会把他们赋值给某个generation然后返回给应用程序,应用就可以在这里面分配对象。
jdk1.8 垃圾收集器_jdk8可以使用G1收集器吗 (https://mushiming.com/)  第1张

young区包含了eden区(红色表示)和survivor区(标有S的红色)。这些region和其他收集器里面的连续的内存空间的作用是一样的,不一样的地方在于:G1里面,这些region在内存里面可以是物理上不连续的。old region(浅蓝色表示)组成了old区。old区的region有可能是大对象区(humongous )(标有H的浅蓝色表示),这些对象可以横跨多个region。

应用总是在young区分配对象,更确切地说是在eden区,大对象除外,因为大对象是直接分配在old区。

G1在停顿的时候可以回收整个young区的内存,在任何停顿都可以回收一些可选的old区的内存。停顿的时候,G1会把对象从一个地方复制到堆里面的一个或者多个region,对象要拷贝到的目的地region取决于来源region:来自young区的对象要么被拷贝到survior区要么到old区,来自old区的对象会从一个old region拷贝到另一个old region,不同的old region是有不同的年龄的。

垃圾收集的生命周期

宏观上看,G1的垃圾回收在两个阶段中来回交替执行的。young-only阶段会逐步把old区填满存活对象,space-reclamation阶段除了会回收young区的内存以外,还会增量回收old区的内存。然后就会重新开始young-only阶段。

图9-2用一个可能会发生的回收停顿的例子描述了这个周期:

jdk1.8 垃圾收集器_jdk8可以使用G1收集器吗 (https://mushiming.com/)  第2张

下面的章节详细的讲述了G1回收周期中的这些阶段、停顿、还有阶段之间的变迁:

Young-only阶段

这个阶段从young区的一些普通的young GC开始,这些GC会把young区的对象晋升到old区。当old区的占有率达到一个阈值的时候就开始young-only阶段到space-reclamation阶段的转换,这个时候,就开始Concurrent Start的young GC而不是普通的young GC。

  • Concurrent Start : 这种类型的GC除了做普通的young GC以外,还会开始标记(marking)过程。并发标记会找出当前old区中所有的存活的对象以备随后的 space-reclamation阶段来使用。当并发标记还没结束的过程中,还是可能会发生普通young GC的。标记经过两个特殊的STW停顿之后才会结束:重新标记(remark)和清理(cleanup)。
  • 重新标记(Remark): 这个停顿中会结束掉mark阶段、做全局的引用处理和类卸载,回收全空的region和清理掉内部的数据结构。在重新标记(remark)和清理(cleanup)之间,G1会并发的在选中的old区的region中计算随后可以回收的空间,这个计算会在Cleanup的时候结束。
  • 清理(Cleanup)。这个停顿中会决定是否要开始space-reclamation阶段。如果要开始space-reclamation, young-only阶段就结束了,紧接着就开始一个Mixed young GC。

Space-reclamation阶段

这个阶段由多个Mixed GC组成,不光是回收young区的region,同时也会回收old区的region。当G1发现,无法回收更多old区的region的时候,space-reclamatio阶段就结束了。

space-reclamation阶段以后,会以young-only阶段开始一个新的回收周期。为了以防万一,在收集存活对象的时候,如果应用的内存耗尽了,G1也会像其他的收集器一样做整个堆的STW的堆压缩(也就是FullGC)。

G1的内部实现

本节描述了G1收集器的一些重要的细节。

如何确定初始标记的阈值

Initiating Heap Occupancy Percent (IHOP) 是触发开始初始标记的阈值,它的值是old区大小的一个百分比。通过观察标记过程使用的时间和标记过程中old区分配的内存大小,G1默认会自动的调整一个最优的IHOP,这个特性就叫做自适应的IHOP。如果激活了这个特性, -XX:InitiatingHeapOccupancyPercent 这个选项决定了IHOP的初始值,因为初始的时候还没有足够的数据来对这个值做预测。-XX:-G1UseAdaptiveIHOP这个选项可以关闭自适应,此时,由 -XX:InitiatingHeapOccupancyPercent设置的阈值就始终不会改变。

自适应IHOP会这样来设置这个阈值: 当开始space-reclamation阶段的第一个Mixed GC的时候,old区的占有率=old区的最大值减去 -XX:G1HeapReservePercent

标记(Marking)

G1使用SATB算法来做标记。在初始标记开始的时候,G1会保存堆的一份虚拟镜像,初始标记开始时候存活的对象在后续的标记过程中仍然被认为是存活的。这意味着就算是标记过程中这部分对象死亡了,对于space-reclamation阶段来说它们仍然是存活的(有少部分例外)。跟其他的收集器相比,这会导致一些额外的内存被错误的占用。但是,SATB给Remark提供了更低的延迟。那些mark中死亡的对象在下一次mark中会被回收掉。关于mark的一些问题可以参考G1调优

内存非常紧张时候的表现

当应用的存活对象占用了大量的内存,无法容纳回收剩余的对象的时候,就会发生evacuation failure。 Evacuation failure发生的时候,G1为了完成当前的GC,它会保持已经位于新的位置上的存活对象,仅仅是调整对象之间的引用,而不会复制或者移动这些对象。Evacuation failure会导致一些额外的开销,但是一般会跟别的young GC一样快。evacuation failure完成以后,G1会跟往常一样继续恢复应用的执行。G1会假设 evacuation failure是发生在GC的后期,也就是说,大部分对象已经移动过了,已经有足够的剩余内存来继续执行应用程序一直到mark结束 space-reclamation开始。

如果这个假设不成立,G1最终会发起Full GC。这种类型的GC会对整个堆做压缩,可能会非常非常的慢!

关于allocation failure或者是Full GC的而更多信息可以参考G1调优

大对象(Humongous Objects)

大对象是说那些对象大小超过了region一半的对象。如果没有设置 -XX:G1HeapRegionSize这个选项,当前region的大小自适应计算的,在G1自适应默认值这一节有详细讲述。

G1对这些大对象会做特殊处理:

  • 大象是分配在old区的一系列连续的region中,对象的开始总是位于这一系列region的第一个region的开始,在整个对象被回收掉之前,最后一个region中剩余的空间都不会被分配出去。

  • 一般来说,只有在Cleanup停顿阶段mark结束以后或者FullGC的时候,死亡的大对象才会被回收掉。但是,基本类型的数组的大对象是例外的,比如bool数组、所有的整形数组、浮点型数组等。G1会在任何GC停顿的适当时候回收这些大对象,如果他们不再有引用。这个默认是开启的,但是可以使用 -XX:G1EagerReclaimHumongousObjects这个选项禁用掉。

  • 分配大对象会导致过早的发生GC停顿。G1在分配每一个大对象的时候都会去检查IHOP,如果当前的堆占用率超过了IHOP阈值,就会强制立即发起一个初始标记的young GC。

  • 即使是在FullGC的时候,这些大对象也是永远不会被移动的。这会导致过早的发生FullGC或者是意外的OOM,尽管此时还有大量的空闲内存,但是这些内存都是region中的内存碎片。

Young-Only阶段Generation的大小变化

在young-only阶段,要回收的那些region集合(回收集)只由young区的
region组成。在每一个普通的youngGC结束的时候,G1总是会调整young区的大小。基于长期的对实际停顿时间的观察,G1就可以满足 -XX:MaxGCPauseTimeMillis-XX:PauseTimeIntervalMillis 设置的停顿时间的目标。它会计算出来回收相同大小的young区的内存需要花费多少时间,其中包括在GC的时候要拷贝多少个对象,对象之间是如何相互关联的。

如果没有其他的限定条件,G1会把young区的大小调整为 -XX:G1NewSizePercent-XX:G1MaxNewSizePercent 之间的值来满足停顿时间的要求。关于如何解决长时间停顿问题可以参考G1调优

Space-Reclamation阶段Generation的大小变化

在space-reclamation阶段,G1会尽量在一个GC停顿之内回收尽可能多的old区的内存。young区的大小被调整为 -XX:G1NewSizePercent设置的允许的最小值,只要是存在可回收内存的old区的region都会被添加到回收集合中,一直到再增加就会超出停顿时间的目标为止。在特定的某个GC停顿之内,G1会按照这些region回收的效率来添加要回收的region,效率高的排在前面,得到最终要回收的region集合。

每一个GC停顿要回收的old区的region数量受限于候选region集合数量除以 -XX:G1MixedGCCountTarget这个选项设置的长度,候选region集合是old区的所有的占用率低于 -XX:G1MixedGCLiveThresholdPercent 的那些region。

当候选region集合中可回收的空间低于 -XX:G1HeapWastePercent 的时候,这一阶段就结束了。

关于G1会使用old区的多少个region和如何避免长时间的mixed停顿可以参考G1调优

G1自适应的一些默认值

本节讲述了G1的一些重要的默认行为和默认值,讲述了如果不做额外的处理,G1的预期的行为和资源的占用情况。

选项和默认值 描述
-XX:MaxGCPauseMillis=200 最大停顿时间的目标
-XX:GCPauseTimeInterval=< ergo> 最大停顿时间间隔目标。G1默认不会设置任何目标
-XX:ParallelGCThreads=< ergo> 设置GC停顿时候的并行的GC收集线程数。它是根据虚拟机所在的主机的可用CPU线程数来计算的:如果CPU少于8个这个值就是cpu的数量,否则,就等于cpu数量*5/8。每个停顿开始的时候,最大的GC线程数还受限于最大的堆内存,G1的内个线程能使用的最大堆内存是由-XX:HeapSizePerGCThread来设置的
-XX:ConcGCThreads=< ergo> 设置与应用并发执行的GC线程数,默认是-XX:ParallelGCThreads/4
-XX:+G1UseAdaptiveIHOP-XX:InitiatingHeapOccupancyPercent=45 默认自适应IHOP是开启的,前几个GC周期G1会使用 old区占用45%作为开始mark的阈值
-XX:G1HeapRegionSize=< ergo> region的大小,整个堆大概有2048个region,region的大小可以在1-32M之间,必须是2的次方
-XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60 young区的大小就在这两个值之间变化
-XX:G1HeapWastePercent=5 候选region集合中可以不被回收的空间数量,如果候选region集合的空闲空间数量低于这个值G1就会终止space-reclamation阶段
-XX:G1MixedGCCountTarget=8 space-reclamation阶段需要做多少次GC
-XX:G1MixedGCLiveThresholdPercent=85 如果存活的对象占有率超过了这个值,space-reclamation阶段不会回收这个region

注意:< ergo>的意思是实际的值是根据环境自适应确定的。

跟其他收集器的比较

G1和其他收集器的主要区别:

  • 并行收集器(Parallel GC)可以压缩和回收old区的内存,但是只能对old区整体来操作。G1可以把整个工作增量的分散到多个时间更短的停顿中。这在减少停顿时间的同时会牺牲一部分吞吐量。
  • 跟CMS类似,G1是并发的回收old区的内存,但是,CMS不会处理old区的碎片,最终就会导致长时间的FullGC。
  • 由于并发的原因,G1可能会表现出比其他收集器更高的开销,这会影响吞吐量。

基于它的工作原理,G1有多种提高GC效率的机制:

  • G1在任何的停顿都可以回收一些全空或者大量的old区的内存。这会避免不必要的GC,因为可以不费吹灰之力就可以释放大量的内存空间。
  • G1可以选择对整个堆里面的String进行并行去重。

回收old区的空的、大对象是默认开启的,可以使用 -XX:-G1EagerReclaimHumongousObjects 这个选项禁用掉。String去重默认是不开启的,可以使用 -XX:+G1EnableStringDeduplication 开启。

最后再插播一次广告:欢迎观看本人录得两个视频教程:

  1. Java生产环境下性能监控与调优详解 里面还有很多监控调优的手段
  2. Java秒杀系统方案优化 高性能高并发实战
THE END

发表回复