原文:https://docs.oracle.com/javase/9/gctuning/garbage-first-garbage-collector.htm#JSGCT-GUID-0394E76A-1A8F-425E-A0D0-B48A3DC82B42
G1垃圾收集器的主要服务对象是大内存多核心的服务器。G1的目的是在尽可能不需要配置的情况下去保持高吞吐量和低停顿时间。G1旨在为拥有以下特征的应用在运行时达到最短的延迟和更高吞吐量平衡点 :
G1取代了CMS收集器,同时也是JDK9的默认收集器。下面我们将介绍G1是如何做到在控制gc时间的同时又保持着如此高的吞吐量的:
jdk9默认就是G1收集器,我们可以显示得使用-XX:+UseG1GC
来打开G1
G1是一个分代分步的并行收集器,并且他也会有stop-the-world问题。与其他收集器类似,G1将堆内存分为新生代和老年代。内存回收主要发生在新生代,而在老年代中也会间歇性回收内存。
为了提高吞吐量G1将一些指定操作只在stw阶段进行,例如全局标记这样耗时间的操作会和用户的应用一起并发进行。为了降低垃圾回收时的stw暂停时长,G1通过并行分步的方式进行垃圾回收。G1通过对应用和垃圾回收的行为记录进行建模,从而达到可预测的回收停顿时间,并且通过这些数据来衡量暂停时候完成的工作量。举例来说,G1首先在回收效率最高的区域(垃圾最多的区域)进行回收,G1通过分散法来回收空间:将存活对象复制分散到新的内存区域,并且在此过程中进行对象整理。在此之后,之前被占据的那一片区域都会释放出来。
G1收集器其实并非实时垃圾回收器。他的目标是在长期运行的应用中达到设置的目标暂停时间。
G1将堆内存划分为一系列同等大小的区块,每一片区块就是如下图所示的一片连续的虚拟内存空间。其中一个区块就是垃圾回收和内存分配的标准单元。浅灰色的部分代表空白未分配部分,其他颜色的则代表新生代和老年代。当有内存请求来到时,内存管理器会找到空白的部分并将其分配到指定的年代后然后返回给我们应用使用。
年轻代包括eden区(红色部分)和survior区域(红色标记为s部分)。这两个区域与其他收集器中的eden和survior提供了同样的功能,但是有所区分的是G1中的内存区域不是连续的。淡蓝色部分的块组成了老年代,老年代的划分可能会相对大一些(比如带有H字样的块),这样设置的目的是因为有一些大的对象会横跨多个区域。
正常情况下应用总是会分配到新生代,也就是eden区,但是例外的是大对象会被直接分配到老年代。
G1会对整个新生代进行垃圾回收,并且同时对部分需要回收的老年代也进行回收。在回收阶段G1将一个区块中的对象拷贝到一个或者多个不同的区块,目标区块由被回收对象所在的区块决定:整个新生代存活的对象将被拷贝到survior或者老年代,老年代的对象拷贝通过年代的年龄再具体区分。
大致来说,G1回收期存在两个周期。年轻阶段是值在老年代区域没达到指定阈值情况下的独立垃圾回收。空间回收阶段是G1在从新生代回收内存以外,再从老年代逐步回收内存的阶段。两个阶段循环执行。
以下列表展示了不同的阶段之间的转换:
年轻阶段: 该阶段随着一些年轻代的对象逐步晋升到老年代而开始。当老年代的内存占用率达到一定阈值这两个阶段会发生转换。此时,G1会初始化一次对年轻阶段回收的标记,称为initial mark。
该部分描述了一些G1的细节原理
初始的堆占用值(IHOP) 是用来启动标记回收的阈值,该值表示老年代空间被占用的百分比。
默认情况下G1会在标记循环中通过标记使用的时间和老年代内存的分配值来自动算出最合适的初始占用值。该特性被称作Adaptive IHOP。如果启用了该特性,我们就可以用-XX:InitiatingHeapOccupancyPercent
来决定IHOP的初始值,因为一开始G1是没办法通过之前的循环算出该值的。我们也可以使用-xx:-G1UseAdaptiveIHOP
选项来禁用该特性,这样我们设置的默认初始值就会一直生效。
G1的标记过程使用了一个叫做Snapshot-At-The-Beginning (SATB)的算法。在启动标记暂停的时候,他会为当前堆做一个虚拟快照,在标记开始时存活的对象会被认为在接下来的标记过程中一直存活。这意味着即使该对象在标记过程中已经死亡我们还是认为他是存活的。与其他收集器相比,这一策略可能会导致我们保留了一些已经失效的对象。但是尽管这样SATB在Remark阶段依然提供了更好的延迟,况且这些被误保留的对象在下一次标记中一样会被清楚。
当应用中存活对象非常多的时候,我们的分散法没法找到足够的空间去复制当前对象,这时候分散就失败了。这意味着G1将会保留那些已经被拷贝的对象,并同时不再移动未被拷贝的对象,仅仅去调整已经移动的对象间的引用。分散失败也许还会导致一些其他的性能开销,但是大部分情况下是没什么问题的。分散失败后,G1会恢复程序运行并不采取任何其他措施。G1默认分散失败大都是在垃圾回收的结尾阶段才会发生,也就是说G1认为大部分该移动的对象已经被移动过了,所以并不影响下一次的标记和回收。
如果这种假设不能成立,G1就会最终启动一次full gc。在Full gc的同时会对整个堆进行整理,过程会十分的慢。
这里的大对象是值超过半个块区域大小的对象。当前的块区块大小是默认设置的,除非我们显示指定,可以通过-xx:G1HeapRegionSize
选项进行。
大对象有的时候会被进行特殊对待:
在年轻回收阶段, 需要进行回收的区块,仅仅由年轻代组成. 在此阶段,G1通过长期观察实际的暂停时间,相同大小的年轻代分散使用的时间,来最终达到我们设定的-XX:MaxGCPauseTimeMillis
和-XX:PauseTimeIntervalMillis
目标。期间还会检查多少对象需要拷贝,以及这些对象之间是如何关联的。
如果这样行不通,那么G1会在设置的-XX:G1NewSizePercent
和 -XX:G1MaxNewSizePercent
之间自适应调整年轻代大小从而来争取达到目标暂停时间。
在空间回收阶段,G1会尝试通过一次暂停来尽可能多得回收老年代空间。年轻代的大小会被设置为最小允许值,并且会添加所有需要进行收集的老年代区块直到G1认为无法在指定时间内收集完这些区块。在暂停期间,G1通过区块的回收效率和剩余的可用回收时间来决定最终要回收的区块集。每次选中的回收块都是从需要回收的候选区块中选出的,候选者的标准是存活比例低于-XX:G1MixedGCLiveThresholdPercent
的区块。
当候选区域中未回收的比例低于-XX:G1HeapWastePercent
设置的值的时候,此阶段便告一段落。
-XX:MaxGCPauseMillis
=200 最大可暂停时间-XX:GCPauseTimeInterval=
暂停间隔时间,默认由G1自己控制,允许背靠背执行-XX:ParallelGCThreads=
STW线程数,低于8核心建议设置为核心数,大于的可以设置为核心数量的8分之5-XX:ConcGCThreads=
设置并行标记的线程数。将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4 左右。-XX:+G1UseAdaptiveIHOP -XX:InitiatingHeapOccupancyPercent=45
设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。-XX:G1HeapRegionSize=
设置的 G1 区域的大小。值是 2 的幂,范围是 1 MB 到 32 MB 之间。目标是根据最小的 Java 堆大小划分出约 2048 个区域。-XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60
设置要用作年轻代大小最大值的堆大小百分比。-XX:G1HeapWastePercent=5
设置您愿意浪费的堆百分比。当比例低于此值,G1就会停止回收-XX:G1MixedGCCountTarget=8
设置标记周期完成后,对存活数据上限为 G1MixedGCLIveThresholdPercent
的旧区域执行混合垃圾回收的目标次数。默认值是 8 次混合垃圾回收。混合回收的目标是要控制在此目标次数以内-XX:G1MixedGCLiveThresholdPercent=85
老年代中存活对象比例超过此值的区块不会被放入回收阶段。-XX:+G1EnableStringDeduplication
来打开此特性。