G1垃圾收集器

G1垃圾收集器

原文:https://docs.oracle.com/javase/9/gctuning/garbage-first-garbage-collector.htm#JSGCT-GUID-0394E76A-1A8F-425E-A0D0-B48A3DC82B42

G1垃圾收集器介绍

      G1垃圾收集器的主要服务对象是大内存多核心的服务器。G1的目的是在尽可能不需要配置的情况下去保持高吞吐量和低停顿时间。G1旨在为拥有以下特征的应用在运行时达到最短的延迟和更高吞吐量平衡点 :

  • 超过10GB的堆内存,并且堆中有一半以上的存活数据
  • 对象的分配和晋升随着时间变化很大
  • 堆内存在大量的内存碎片
  • 需要可预测的gc时间,希望gc不会超过几百毫秒,从而避免长时间的gc停顿

      G1取代了CMS收集器,同时也是JDK9的默认收集器。下面我们将介绍G1是如何做到在控制gc时间的同时又保持着如此高的吞吐量的:

打开 G1收集器

jdk9默认就是G1收集器,我们可以显示得使用-XX:+UseG1GC 来打开G1

基本概念


      G1是一个分代分步的并行收集器,并且他也会有stop-the-world问题。与其他收集器类似,G1将堆内存分为新生代和老年代。内存回收主要发生在新生代,而在老年代中也会间歇性回收内存。
      为了提高吞吐量G1将一些指定操作只在stw阶段进行,例如全局标记这样耗时间的操作会和用户的应用一起并发进行。为了降低垃圾回收时的stw暂停时长,G1通过并行分步的方式进行垃圾回收。G1通过对应用和垃圾回收的行为记录进行建模,从而达到可预测的回收停顿时间,并且通过这些数据来衡量暂停时候完成的工作量。举例来说,G1首先在回收效率最高的区域(垃圾最多的区域)进行回收,G1通过分散法来回收空间:将存活对象复制分散到新的内存区域,并且在此过程中进行对象整理。在此之后,之前被占据的那一片区域都会释放出来。
      G1收集器其实并非实时垃圾回收器。他的目标是在长期运行的应用中达到设置的目标暂停时间。

堆内存布局

      G1将堆内存划分为一系列同等大小的区块,每一片区块就是如下图所示的一片连续的虚拟内存空间。其中一个区块就是垃圾回收和内存分配的标准单元。浅灰色的部分代表空白未分配部分,其他颜色的则代表新生代和老年代。当有内存请求来到时,内存管理器会找到空白的部分并将其分配到指定的年代后然后返回给我们应用使用。

G1垃圾收集器_第1张图片

      年轻代包括eden区(红色部分)和survior区域(红色标记为s部分)。这两个区域与其他收集器中的eden和survior提供了同样的功能,但是有所区分的是G1中的内存区域不是连续的。淡蓝色部分的块组成了老年代,老年代的划分可能会相对大一些(比如带有H字样的块),这样设置的目的是因为有一些大的对象会横跨多个区域。
      正常情况下应用总是会分配到新生代,也就是eden区,但是例外的是大对象会被直接分配到老年代。
      G1会对整个新生代进行垃圾回收,并且同时对部分需要回收的老年代也进行回收。在回收阶段G1将一个区块中的对象拷贝到一个或者多个不同的区块,目标区块由被回收对象所在的区块决定:整个新生代存活的对象将被拷贝到survior或者老年代,老年代的对象拷贝通过年代的年龄再具体区分。

循环垃圾回收

      大致来说,G1回收期存在两个周期。年轻阶段是值在老年代区域没达到指定阈值情况下的独立垃圾回收。空间回收阶段是G1在从新生代回收内存以外,再从老年代逐步回收内存的阶段。两个阶段循环执行。
G1垃圾收集器_第2张图片
以下列表展示了不同的阶段之间的转换:

      年轻阶段: 该阶段随着一些年轻代的对象逐步晋升到老年代而开始。当老年代的内存占用率达到一定阈值这两个阶段会发生转换。此时,G1会初始化一次对年轻阶段回收的标记,称为initial mark。

  • Initial Mark : 在年轻代回收以外要开始一次标记过程。并通过发标记来决定所有要被保留的老年代存活对象(为下一个回收阶段准备)。在标记未完成的时候,年轻代回收依然会继续执行。在两次特殊的stw停顿后,标记完成,这两次停顿分别是: Remark和Cleanup.
  • Remark: Remark会完成所有的标记,并且进行全局关联和类卸载。在Remark和cleanup之间G1会并发得计算总结出所有的存活信息,这些信息在cleanup暂停中会用来更新内部数据。
  • Cleanup: 该阶段会回收所有的空白区域,并且决定是否接下来要进行空间回收动作。如果接下来需要进行空间回收,将通过一次年轻代的回收动作来完成。
          回收阶段: 在年轻代区域之外,此阶段由若干个次混合的垃圾回收组成,同时分散老年代里的存活对象。随着分散回收的进行,当G1觉得分散更多的老年代不会产生与之相匹配的空白空间时,回收阶段便告一段落。
          在回收阶段后,回收循环重启,再次来到年轻阶段。另外作为候补机制,当应用内存不足以用来收集所需存活对象信息的时候,G1会启动一次full gc。

G1内部原理

该部分描述了一些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选项进行。
大对象有的时候会被进行特殊对待:

  • 每个大对象都被连续分配在老年代。对象的起始总是被分配在序列中第一个区块的起始。区块中剩下的空间将不再使用,除非该对象被回收。
  • 通常来说,大对象只有在清理暂停的标记末端或者Full gc中才会被清除。但是有个例外便是,对于基本类型的数组对象G1有机会在任意回收阶段对该数组进行回收。此特性默认打开,可以通过XX:G1EagerReclaimHumongousObjects进行关闭。
  • 大对象的分配可能会导致gc过早发生,每次一有大对象分配,G1就会检查IHOP从而可能导致标记过程的发生。
  • 大对象在gc过程中是不会移动的,即便是full gc,这也可能导致full gc非常缓慢甚至oom问题,因为大对象的存在会导致大量的内存碎片。

年轻代的回收标准

      在年轻回收阶段, 需要进行回收的区块,仅仅由年轻代组成. 在此阶段,G1通过长期观察实际的暂停时间,相同大小的年轻代分散使用的时间,来最终达到我们设定的-XX:MaxGCPauseTimeMillis-XX:PauseTimeIntervalMillis目标。期间还会检查多少对象需要拷贝,以及这些对象之间是如何关联的。
      如果这样行不通,那么G1会在设置的-XX:G1NewSizePercent-XX:G1MaxNewSizePercent之间自适应调整年轻代大小从而来争取达到目标暂停时间。

空间回收的一些标准

      在空间回收阶段,G1会尝试通过一次暂停来尽可能多得回收老年代空间。年轻代的大小会被设置为最小允许值,并且会添加所有需要进行收集的老年代区块直到G1认为无法在指定时间内收集完这些区块。在暂停期间,G1通过区块的回收效率和剩余的可用回收时间来决定最终要回收的区块集。每次选中的回收块都是从需要回收的候选区块中选出的,候选者的标准是存活比例低于-XX:G1MixedGCLiveThresholdPercent的区块。
      当候选区域中未回收的比例低于-XX:G1HeapWastePercent设置的值的时候,此阶段便告一段落。

G1 默认参数

  • -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 老年代中存活对象比例超过此值的区块不会被放入回收阶段。
    Note:
    ** 意味着通过具体环境具体配置。

与其他收集器的比较:

  • Parallel GC只能对整个老年代进行整理回收。G1可以将该过程分布到多个区域执行从而压缩gc时间,当然也有可能会导致吞吐量的减少。
  • 与CMS类似,G1并发得收集老年代,尽管如此,CMS不能对老年代进行碎片整理,最终容易触发full gc。
  • G1并发特性有可能会比其他收集器产生更高的开销,从而影响吞吐量.
    G1有一些特性来提高效率:
  • G1可以在任意回收阶段回收老年代的完整的空白区块。这可以避免很多不必要的回收动作,并且以很低的代价释放出大量空间。
    G1可以选择性的在java堆中对符串进行去重。可以通过-XX:+G1EnableStringDeduplication来打开此特性。

你可能感兴趣的:(java,jvm,G1)