关于java:JVM知识梳理之三内存分配与垃圾收集

4次阅读

共计 30434 个字符,预计需要花费 77 分钟才能阅读完成。

本文就 JVM 的内存调配与垃圾收集,做一些简略的梳理。

一、内存主动治理概述

内存如何调配取决于 JVM 应用哪种垃圾收集 (GC) 策略。垃圾收集策略事实上就是内存主动治理的策略。这里说的内存,特指 JVM 的堆区和办法区。这里说的垃圾收集策略,是对什么内存须要回收,什么时候回收,如何回收这三件事件的对立形容。

1.1 GC 区域

在《JVM 常识梳理之一_JVM 运行时内存区域与 Java 内存模型》中,曾经形容了 JVM 运行时的五个内存区域:程序计数器,虚拟机栈,本地办法栈,堆,办法区。

其中,程序计数器,虚拟机栈,本地办法栈这三个区域是线程公有的,其内存调配多少与生命周期基本上是编译期可知的,所以它们的内存调配和回收是比拟固定的。因而它们不在 JVM 的垃圾收集策略的范畴内。

而 Java 堆与办法区是线程共享的,它们外部存储的是对象,常量,类信息等等,它们是动静的,不确定的,只有到了运行期能力晓得具体加载了多少类,创立了多少对象。因而 JVM 的垃圾收集策略针对的就是堆和办法区的内存回收。

办法区的垃圾收集绝对固定且较少,因而更多的时候,JVM 的 GC 特指的是 Java 堆的内存治理与回收。

1.2 GC 策略决定了内存如何调配

之前的《JVM 常识梳理之一_JVM 运行时内存区域与 Java 内存模型》中,梳理了 JVM 运行时内存区域的划分,并在讲述 Java 堆区的时候,简略讲了分代收集的大抵流程。

分代收集算法大抵过程:

  1. JVM 新创建的对象会放在 eden 区域。
  2. eden 区域快满时,触发 Minor GC 新生代 GC,通过可达性剖析将失去援用的对象销毁,剩下的对象挪动到幸存者区 S1,并清空eden 区域,此时 S2 是空的。
  3. eden 区域又快满时,再次触发 Minor GC,对edenS1的对象进行可达性剖析,销毁失去援用的对象,同时将剩下的对象全副挪动到另一个幸存者区 S2,并清空edenS1
  4. 每次 eden 快满时,反复上述第 3 步,触发 Minor GC,将幸存者在S1S2之间来回倒腾。
  5. 在历次 Minor GC 中始终存活下来的幸存者,或者太大了会导致新生代频繁 Minor GC 的对象,或者 Minor GC 时幸存者对象太多导致 S1S2放不下了,那么这些对象就会被放到老年代。
  6. 老年代的对象越来越多,最终会触发 Major GCFull GC,对老年代甚至整堆的对象进行清理。通常 Major GCFull GC会导致较长时间的 STW,暂停 GC 以外的所有线程,因而频繁的Major GCFull GC会重大影响 JVM 性能。

这个流程实际上也是 JVM 目前支流的内存调配和垃圾收集的过程。

从下面的流程能够看出,正是因为采取了分代收集的垃圾收集策略,JVM 分配内存时才须要依照 eden 区域,幸存者区域,老年代这样的划分去分配内存。JVM 的内存调配是由垃圾收集策略决定的,或者说,内存调配和垃圾收集是一体的,都属于 JVM 的 内存主动治理。因而关键在于垃圾收集策略。确定了垃圾收集策略,也就天然确定了内存如何调配。所以本文其实就是在梳理 GC 相干常识。

二、如何找到须要回收的内存

GC 针对的内存区域是堆和办法区。堆中寄存着 JVM 简直所有的对象实例,所以 GC 的第一件事件,就是要确定哪些对象实例还 ” 存活 ” 着,哪些曾经 ” 死去 ”。” 死去 ” 即没有任何路径能再应用到这个对象实例。另外,办法区中的废除常量和不再应用的类型,也是能够回收的。

2.1 对象存活判断

如何判断一个对象是否处于存活状态,有两种根本的算法:援用计数算法,可达性剖析算法。

2.1.1 援用计数算法

援用计数算法思路很简略:在对象中增加一个援用计数器;每当被援用时,计数器就加 1;每当失去一个援用时,计数器就减 1;只有计数器为零,该对象就不能再被应用。

这个算法目前根本没有支流的 Java 虚拟机采纳,理解一下即可。起因是,尽管思路简略,但实现起来有很多例外场景,须要很多额定解决。比方对象之间的循环援用。

只有两个对象互相循环援用的话,其实也意味着这两个对象都无奈被其余中央应用了,也就意味着能够被回收了。但单纯的援用计数无奈解决这个问题。

2.1.2 可达性剖析算法

目前支流的 Java 虚拟机,都是通过可达性剖析算法来判断对象是否存活的。

可达性剖析算法基本原理

可达性剖析 (Reachability Analysis) 采纳图论算法,从一些被称为 GC Roots 的根对象登程,依据援用关系向下推导能够达到的对象,造成 援用链 (Reference Chain),也叫对象图。如果某个对象与GC Roots 之间没有任何援用链相连,就认为从 GC Roots 到该对象是不可达的。不可达的对象即不可能再被应用,是能够被回收的对象。可达性剖析算法能够轻松解决循环援用的问题,如下图所示:

很显然,可达性剖析算法的关键点有二:一是 GC Roots 的确定;二是对象间援用关系的确定。

在 JVM 中,GC Roots包含:

  • 虚拟机栈中援用的对象,即各个线程当下压入栈中的栈帧 (即办法) 的参数变量,局部变量所援用到的对象。(能够回顾一下《JVM 常识梳理之一_JVM 运行时内存区域与 Java 内存模型》中对虚拟机栈的梳理。)
  • 办法区中的动态变量与常量所援用的对象,比方援用类型动态变量,字符串的字面量。(能够回顾一下《JVM 常识梳理之二_JVM 的常量池》中对字符串常量池的梳理。)
  • 本地办法栈中 JNI 援用的对象。
  • JVM 外部的援用,比方类加载器,一些常驻的异样对象(空指针,内存溢出等),根本数据类型对应的 Class 对象。
  • 被同步锁 (synchronized) 持有的对象。
  • 反映 Java 虚拟机外部状况的 JMXBean、JVM TI 中注册的回调、本地代码缓存等。
  • 分代收集和部分收集的场景里,如果只针对局部区域进行收集,还要思考关联区域中对象对本区域对象的援用。

对于援用关系,大部分状况下,指的都是传统的 强援用 ,对象之间只有 被援用 未被援用 的关系。但有些非凡的场景下,可能须要这样一种像缓存一样的性能:当内存空间还很短缺的时候可能保留一些失去 强援用 的对象,但 GC 之后空间还很缓和的话,就开释它们。因而 Java 还提供了除 强援用 以外的几种非凡的 援用 关系,但它们须要显式应用对应的类。Java 的援用,从强到弱别离是:强援用 (Strongly Re-ference)、软援用(Soft Reference)、弱援用(Weak Reference) 和虚援用(Phantom Reference)。

  • 强援用是最传统的“援用”的定义,是指在程序代码之中普遍存在的援用赋值,即相似 Object obj = new Object() 这种援用关系。
  • 软援用针对的是还有用,但不是必须的对象。在 JVM 将要产生内存溢出异样前,会将被 ” 软援用 ” 的对象列入收集范畴进行二次收集,这样兴许能防止这次的内存溢出异样。软援用通过 SoftReference 类实现,比方new SoftReference(new Object())
  • 弱援用针对的也是还有用,但不是必须的对象,但强度上比软援用更弱。被弱援用关联的对象只能生存到下一次垃圾收集产生为止。通过 WeakReference 类实现。
  • 虚援用也称为“幽灵援用”或者“幻影援用”, 它是最弱的一种援用关系。一个对象是否有虚援用的存在, 齐全不会对其生存工夫形成影响, 也无奈通过虚援用来获得一个对象实例。为一个对象设置虚援用关联的惟一目标只是为了能在这个对象被收集器回收时收到一个零碎告诉。通过 PhantomReference 类实现。

并发可达性剖析

原则上,可达性剖析算法的两个根本步骤 (GcRoots 枚举与援用链推导) 都对一致性有很强的要求,所以都是须要临时解冻所有用户线程的,以防止剖析期间对象援用关系发生变化。

GcRoots 枚举带来的进展是很短暂且固定的,不会随着 Java 堆内存容量的减少而回升。但援用链推导的耗时则是不固定的。如果咱们能够预测到可达对象理论很少,那么援用链推导其实快得很;但当可达对象很多的时候,援用链推导就会随着可达对象数量的回升而越来越耗时,这就会带来 JVM 很长时间的进展(其余用户线程被暂停)。为了解决这个问题,就须要在可达对象比拟多的时候,采纳并发策略来做援用链推导。

如何预测可达对象多还是少,这在前面的分代收集实践中有梳理。

(1) 串行、并行与并发

这里首先须要定义一下探讨 GC 时的几个名词的含意:

  • GC 线程:指的是 JVM 用来执行垃圾收集的相干线程,它与用户线程绝对应。
  • 用户线程:指的是 JVM 用来执行 GC 以外解决的所有线程,它与 GC 线程绝对应。
  • STW:stop the world,指的是 JVM 暂停所有用户线程,引起 JVM 进展。
  • 串行:指的是 JVM 暂停所有用户线程 (STW) 并应用单线程执行 GC 相干解决。
  • 并行:指的是 JVM 暂停所有用户线程 (STW) 并应用多线程执行 GC 相干解决,通常取 CPU 核数个线程或小于 CPU 核数。
  • 并发:指的是 JVM 并不暂停用户线程,而是应用单个或大量线程作为 GC 线程,与用户线程在某个时间段内 ” 一起 ” 执行。此时 GC 线程数量个别管制在 1 / 4 的 CPU 核数,但并不意味着 GC 线程能够 ” 抢到 ”1/ 4 的 CPU 负荷,因为总的线程数往往是远大于 CPU 核数的,它们不会严格地 ” 同时 ” 在运行,总会有 CPU 切换线程执行。

在明确以上定义之后,就能够说,并发可达性剖析,指的是在做可达性剖析 (次要是援用链推导) 时 GC 线程与用户线程并发执行。很显然,并发可达性剖析次要是为了进步援用链推导,即标记过程的效率,充分发挥多核 CPU 的硬件性能。

但并发策略会带来新的问题:对象隐没。这个问题须要引入三色分析法来阐明。

(2) 三色标记法

三色标记是一种辅助援用链推导的办法,它把遍历对象图过程中遇到的对象,依照“是否拜访过”这个条件标记成以下三种色彩:

  1. 红色:示意对象尚未被垃圾收集器拜访过。显然在可达性剖析刚刚开始的阶段,除了 GCRoots,所有的对象都是红色的;但在剖析完结之后,依然红色的对象即代表不可达。
  2. 彩色:示意对象曾经被垃圾收集器拜访过,且这个对象的所有对其余对象的援用也都曾经扫描过。彩色的对象代表曾经扫描过,它是可达的。如果在后续扫描过程中有其余对象援用指向了彩色对象,则毋庸从新扫描一遍。彩色对象不可能间接 (不通过灰色对象) 指向某个红色对象。
  3. 灰色:一种两头长期状态,示意对象曾经被垃圾收集器拜访过,但这个对象上至多存在一个援用还没有被扫描过。

后续梳理的各种垃圾收集器在标记阶段给对象做的可达性剖析后果就是这个三色标记。

(3) 对象隐没

为什么说采纳并发标记的话,可能会有对象隐没景象?能够参考下图了解:

数学上曾经证实了当且仅当以下两个条件同时满足时,会产生“对象隐没”的问题,即本来应该是彩色的对象被误标为红色:

  1. 赋值器插入了一条或多条从彩色对象 (代表曾经彻底扫描过的可达对象) 到某个红色对象 (还没扫描到,或正要被 ” 从灰色对象扫描到对它 ”,这里叫它小白) 的新援用;
  2. 赋值器删除了原先全副的从灰色对象到该红色对象 (小白) 的间接或间接援用。

(4) 增量更新和原始快照

如何解决并发标记中的对象隐没问题?目前有两种计划:增量更新 (Incremental Update) 和原始快照(Snapshot At The Beginning,SATB)。

  • 增量更新要毁坏的是第一个条件,当彩色对象插入新的指向红色对象的援用关系时,就将这个新插入的援用记录下来,等并发扫描完结之后,再将这些记录过的援用关系中的彩色对象为根,从新扫描一次,包含其间接援用和间接援用,这是深度扫描。这能够简化了解为, 彩色对象一旦新插入了指向红色对象的援用之后,它就变回灰色对象了。
  • 原始快照要毁坏的是第二个条件,当灰色对象要删除指向红色对象的援用关系时,就将这个要删除的援用记录下来,在并发扫描完结之后,再将这些记录过的援用关系中的灰色对象为根,从新扫描一次。但留神,这时扫描的仅仅只是被记录下来的原先灰色对象对红色对象的援用,不会扫描其余援用关系。这也能够简化了解为,无论援用关系删除与否,都会依照刚刚开始扫描那一刻的对象图快照来进行搜寻。

原始快照可能会导致理论只被删除援用没有被其余彩色对象新增援用的对象保留下来,成为浮动垃圾,要等到下次 GC 时再做可达性剖析时才会被标记为红色。而增量更新没有这样的问题。但增量更新的效率是没有原始快照高的,因为原始快照的从新扫描仅仅是被删除的 ” 灰色 -> 红色 ” 关系,并不会整个从新扫描灰色对象的所有间接和间接援用关系。

2.1.3 垃圾审判流程

通过 GC Roots 和援用链推导,JVM 就可能判断一个对象是否可达。但要留神,不可达的对象并非肯定会被回收。

在 JVM 的 GC 设计中,真正回收一个对象,这个对象还要经验一个审判过程:两次标记和一次筛选。如下图所示:

第一次标记是可达性剖析后果为不可达,此时它处于“期待秋后问斩”的状态,还有“上访”的机会;筛选是指每个对象都继承了 Object 对象的 finalize() 办法,如果它的类重写了该办法,且之前没有被 JVM 调用过该对象的 finalize() 办法,那么这个对象会退出一个队列期待执行 finalize() 办法;第二次标记是指当 finalize() 被执行时,就是这个对象翻盘援救本人的最初机会,就看它在这个办法里是否与 GC Roots 的援用链从新关联上;如果未能从新关联,或者压根没有本人重写笼罩finalize(),那这个对象就真的“死路一条”了。

2.2 办法区的回收判断

办法区的垃圾收集次要回收两局部内容:废除的常量和不再应用的类型。

废除的常量指的常量池中不再有中央应用的字面量或符号援用。比方一个字符串 "曾经沧海难为水",已经被应用,字面量进入了常量池(运行时常量池与字符串常量池),但以后 JVM 里没有任何一个 String 对象的值是"曾经沧海难为水",那么此时产生 GC 的话,这个"曾经沧海难为水" 字面量就有可能被清理出常量池。

不再应用的类型的判断要简单的多,同时满足上面三个条件,才有可能回收对应的加载过的类信息:

  • 该类所有的实例都曾经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例。
  • 加载该类的类加载器曾经被回收,这个条件只有实现了可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。
  • 该类对应的 java.lang.Class 对象没有在任何中央被援用,无奈在任何中央通过反射拜访该类的办法。

三、分代收集实践与经典垃圾收集器

从如何判断对象存活的角度看,垃圾收集算法能够划分为 援用计数式垃圾收集 (也叫 间接垃圾收集 ),和 追踪式垃圾收集 (也叫 间接垃圾收集 ) 两大类。目前支流的 Java 虚拟机采纳的都是基于 可达性剖析算法 追踪式垃圾收集 策略,且大多都遵循了 分代收集 实践进行设计。

3.1 分代收集实践

分代收集实践建设在三个教训假如之上:

  1. 弱分代假说:绝大多数对象都是朝生夕灭的,生命周期很短。
  2. 强分代假说:熬过越屡次垃圾收集过程 (屡次可达性剖析均可达) 的对象就越难以沦亡。
  3. 跨代援用假说:跨代援用绝对于同代援用来说占比极小。

前两个假说奠定了分代收集的实践根底:垃圾收集器应该将 Java 堆划分为不同的区域,并依据其年龄 (对象熬过垃圾收集的次数) 调配到不同的区域。这样对于比拟年老的区域,GC 更加频繁,因为能够回收更多的内存空间;而对于较老的区域,因为难以沦亡,GC 频率就会较低。这种分代收集实践能够兼顾垃圾收集的工夫开销和内存利用率。

支流 JVM 中,个别至多会把堆划分为新生代 (Young Generation) 和老年代(Old Generation)。联合后面的可达性剖析相干内容,咱们能够推断:新生代的援用链推导会很快,因为理论可达的对象应该不多;老年代的援用链推导会比较慢,因为理论可达对象比拟多。从前面的理论的垃圾收集器来看,对进展工夫比拟在意的垃圾收集器都会在老年代的标记阶段采纳并发标记,即并发可达性剖析算法。

但只有前两个假说会带来一个问题:当独自对一个区域比方新生代进行垃圾收集时,因为新生代的对象有可能被老年代的对象所援用,因而须要在 GC Roots 中增加所有老年代的对象。反过来一样,对老年代进行垃圾收集时,须要将新生代的对象退出 GC Roots。毫无疑问的是,将关联区域的所有对象退出GC Roots 会给垃圾收集带来很大的性能累赘。因而又有了下面的第三个假说。依据第三个假说,跨代援用比拟少,只用在新生代建设一个全局的数据集,将老年代划分为若干小块,标识哪些块上存在跨代援用;当新生代 GC 时,不必将老年代的所有对象都退出 GC Roots,只须要将有跨代援用的块退出即可。当然,这种办法须要在对象援用关系创立或扭转时同时保护这个全局数据集,减少了局部性能开销,但相比将整个老年代退出GC Roots 进行可达性剖析来说,还是很划算的。

第三个假说其实也是前两个假说的隐含论断:存在援用关系的两个对象,应该会偏向于同一个区域。比方,如果新生代某个对象被老年代某个对象援用,那么这个新生代的对象也会经验屡次 GC 后被放到老年代去。

以 HotSpot 为例,它的经典垃圾收集器都是遵循分代收集实践的,将堆划分为新生代和老年代,由此呈现了基于分代的 GC 名词:

  • 局部收集,Partial GC,指仅对局部分代区域进行的垃圾收集,它又能够分为 新生代收集 老年代收集 混合收集
  • 新生代收集,Minor GCYoung GC,针对新生代的垃圾收集。
  • 老年代收集,Major GCOld GC,针对老年代的垃圾收集。(Major GC 有时也指整堆收集 Full GC。)
  • 混合收集,Mixed GC,指标是收集整个新生代以及局部老年代的垃圾收集。目前只有 G1 收集器会有这种行为。
  • 整堆收集,Full GC,针对整个 Java 堆个办法区的垃圾收集。

3.2 根底垃圾收集算法

依据分代收集实践,针对不同区域的不同特点,应该有对应的垃圾收集算法。比方新生代的对象存活率很低,大部分都熬不过第一轮的回收;而老年代的对象往往能熬过很多轮的回收,即存活率比拟高。

JVM 基于分代收集实践的垃圾收集器通常称为经典垃圾收集器,它们针对新生代和老年代,别离应用到了上面的 3 个根底的垃圾收集算法:

  • 标记革除算法
  • 标记复制算法
  • 标记整顿算法

3.2.1 标记革除算法

标记革除 (mark-sweep) 算法是最根底的一种垃圾收集策略,它的思路很简略,将垃圾收集分为 标记 革除 两个阶段:首先标记出所有能够被回收的对象;而后将被标记的对象回收掉。标记阶段采纳可达性剖析,并须要将不可达对象标记为革除对象。整个过程只有标记阶段须要 STW。

标记革除算法图示如下:

标记革除算法尽管简略,但有两个毛病:

  1. 执行效率不稳固,如果 Java 堆中对象太多,且大部分都能够回收,比方新生代,那么就须要大量的标记和革除动作,效率会随着对象数量的减少而升高;
  2. 标记 - 革除动作之后会产生大量的内存碎片,如果有大对象须要调配间断空间,可能会提前触发下一次 GC。

针对标记革除算法的特点,它比拟适宜对象存活率高的场合,比方老年代。新生代因为其对象生存率极低,不适宜应用标记革除算法。

即便是老年代,总是应用标记革除算法也会造成碎片化重大的问题,所以当初垃圾收集器们也很少在老年代间接用标记革除算法。

3.2.2 标记复制算法

针对标记革除算法的毛病,人们提出了标记复制算法:将可用内存按容量划分为大小相等的两块,同一时刻只应用其中一块。当这块用完了就将存活的对象复制到另一块去,而后将满了的那块一次性清空。标记阶段仍然是可达性剖析,但只用标记可达对象。图示如下:

标记复制算法针对的是新生代这样的,每次 GC 大部分对象都会被清理 (存活率低) 的场景。它解决了标记革除算法的两个毛病:因为存活的对象属于多数,因而标记 - 复制的就少;同时复制的时候也顺便清理了内存碎片。

标记复制算法的毛病是空间节约比拟多,总有一半内存用不上。但对于新生代来说,大部分对象都熬不过第一轮 GC,不须要按 1:1 的比例划分内存空间,所以目前支流 JVM 都会应用改进优化后的标记复制算法来回收新生代。这种改进的算法是半区复制策略,它将新生代分为一块较大的 Eden 空间和两块较小的 Survivor 空间,两个 Survivor 总有一个是空的。内存调配到 Eden,GC 时将存活的对象(包含Eden 中的和非空 Survivor 中的)挪动到空的那块 Survivor 去并清空 Eden 和非空Survivor;下次 GC 则将存活对象挪动到另一块Survivor。HotSpot 默认 Eden 和 Survivor 的大小比例为8:1,即总有 10% 的新生代空间是闲暇的。

这种设计的前提是对象存活率低,十分之一的幸存者区足够放下存活下来的对象。如果很可怜,某次新生代 GC 须要复制的对象超过了 Survivor 的大小,那么就将多余的对象挪动到老年代去,这叫调配担保,Handle Promotion。调配担保是这种幸存者区较小的设计的后路,不罕用到,但肯定要有。

复制算法还有一个毛病,复制时也须要 STW。因为复制对象的援用地址产生了变动,须要暂停应用它们的用户线程,并在挪动后更新全副援用地址。但一般来说,采纳复制算法的新生代的存活对象比拟少,所以这个进展很短暂,大部分场景属于 ” 能够不计较 ” 的代价。

3.2.3 标记整顿算法

如前所述,标记革除适宜老年代,标记复制适宜新生代,仿佛足够了。但事实上老年代的垃圾收集只用标记革除的话,还是有内存碎片化的问题的。而标记复制算法也不适宜老年代的垃圾收集。对于老年代,它的对象的存活率很高,如果应用复制算法的话,很显著复制的对象数量会很多,会拉低垃圾收集的效率。而且如果不想节约 50% 的空间,就必须保障有调配担保,但对老年代而言这显然是很难实现的。

所以针对老年代的特点,人们又提出了标记整顿算法(Mark-Compact):可达性剖析标记存活对象,将存活对象向内存区域的一端挪动,这样存活对象在内存区域中就变成了间断散布,而后间接将存活对象边界之外的局部间接革除即可。如下图所示:

标记整顿实质上是对复制的另一种应用,它与标记革除的不同就在于是否挪动存活对象,而不论挪动与否,都是优缺点并存的危险决策:

  1. 如果不挪动间接革除的话,会造成内存碎片化问题,随着垃圾收集轮次的减少,势必会导致 JVM 对内存的调配更加简单,减少 JVM 累赘,升高总的吞吐量。即,不挪动会导致当前内存调配更简单。
  2. 如果挪动的话,那就和复制算法一样,挪动对象的内存地位须要更新所有对该对象的援用。这种操作岂但对 JVM 造成很大的累赘,同时还会 STW。即,挪动会导致本次回收更简单。

在 JVM 的经典垃圾收集器里,Parallel Scavenge的指标在于进步 JVM 整体吞吐量,它就应用了标记整顿算法;而 CMS 的指标在于低提早或者说低进展,它就应用了标记革除算法。

CMS 对内存碎片化也有优化措施,当几轮的 CMS 垃圾收集过后,发现碎片化水平曾经达到某种会影响到内存调配的水平后,会采纳标记整顿算法收集一次。

至此,咱们介绍了三种根底垃圾收集算法,JVM 基于分代收集实践的几个经典垃圾收集器都是基于这三者实现的。

3.3 经典垃圾收集器

这里介绍的经典垃圾收集器指的是 HotSpot 虚拟机中实现的,区别于革命性翻新的低提早收集器 ZGC 与 Shenandoah 的,采纳分代收集实践的垃圾收集器。包含:

  • Serial
  • ParNew
  • Parallel Scavenge
  • Serial Old
  • Parallel Old
  • CMS
  • Garbage First(简称 G1)

除了 G1,前 6 种收集器都只实用于一个区域,要么新生代,要么老年代。JVM 启动时会通过参数指定或默认应用其中两种搭配作为 JVM 的垃圾回收策略,除了 G1。参考下图:

  1. 上图中,收集器之间的连线代表这两个收集器能够搭配应用,色彩雷同代表在一组搭配中。
  2. Serial/ParNew/CMS/Serial Old这四个收集器的应用了同一套分代架构,因而原本它们的新生代和老年代能够随便搭配应用。但 Serial + CMSParNew + Serial Old 这两种搭配关系从 Java8 开始申明废除,在 Java9 中正式移除。
  3. ParNew + CMS + Serial Old三者一起搭配组合应用,其中 Serial Old 是作为 CMS 的备用计划存在的。
  4. Parallel ScavengeParallel Old 采纳了新的分代架构,跟 Serial/ParNew/CMS/Serial Old 的分代架构不一样,所以它们是不能与 Serial/ParNew/CMS/Serial Old 搭配应用的。但 Parallel Scavenge + Serial Old 这个搭配比拟非凡:理论与 Parallel Scavenge 这个新生代收集器搭配的其实并不是 Serial Old 收集器,而是 Parallel Scavenge 中从新对 Serial Old 的实现,叫 PS MarkSweep。只是因为PS MarkSweep 的实现只是接口变动了,实际上与 Serial Old 共用了大部分实现代码。因而在这张图里将它们作为搭配组合连接起来了。

上面开始梳理这几种垃圾收集器。

3.3.1 Serial 收集器

Serial 收集器采纳标记复制算法对新生代进行垃圾收集,GC 采纳单线程,并 STW,如下图所示:

Safepoint,平安点,是指用户线程所执行的代码指令流可能停下来以进行垃圾收集的地位。GC 线程会等到所有用户线程都达到最近的平安点后再开始执行。

Serial 收集器是最根底的收集器,尽管简略且 STW 工夫绝对较长,但在单核或内存受限的环境下,反而是很高效的一个垃圾收集器。例如一个只调配了单核 CPU 和较小内存 (1G 以内) 的虚拟机上运行的客户端模式的 JVM,就很适宜应用 Serial 收集器。

3.3.2 ParNew 收集器

ParNew 就是 Serial 的 GC 多线程并行版本。如下图所示:

G1 成熟之前,支流的 Java 服务都会采纳 ParNew + CMS 组合作为垃圾收集策略。之所以新生代用 ParNew 其实是因为实现框架的起因,目前只有 ParNew 能和 CMS 配合应用。如果老年代不应用 CMS 的话,那就也不会应用 ParNew 作为新生代垃圾收集器。

ParNew 在单核或低核环境下是不如 Serial 的,因为多线程 GC 并行的话,会有线程间切换的损耗。而多核环境下,其实新生代有更好的垃圾收集器抉择。

3.3.3 Parallel Scavenge 收集器

Parallel Scavenge也是新生代收集器,与 ParNew 在执行时基本相同,同样是基于标记复制算法实现,同样是并行收集。它与 ParNew 的不同之处在于:

  1. 可能通过参数管制 JVM 吞吐量。
  2. 具备自适应调节策略,把内存治理的调优工作交给虚拟机本人实现。
  3. 因为底层框架不同的起因,导致不能和 CMS 配合应用。

(1) 管制 JVM 吞吐量

Parallel Scavenge与其余收集器重点关注如何缩小单次 GC 的进展工夫不同,它更关注的是如何进步 JVM 吞吐量。所谓 JVM 吞吐量,Throughput,它指的是用户线程运行工夫占 JVM 总运行工夫的比例。其公式如下:

吞吐量 = 用户线程运行工夫 / (用户线程运行工夫 + GC 工夫)

它是通过以下两个参数管制吞吐量:

  • -XX:MaxGCPauseMillis : 最大垃圾收集进展工夫,该值是一个目标值,JVM 会尽量管制每次垃圾回收的工夫不超过这个值。要留神,并不是说这个值设置的越小,GC 就越快!该值设置越小的话,Parallel Scavenge触发 GC 的频率就越高,因为这样每次须要收集的垃圾就越少,从而保障工夫不超过设定值。这样的话,每次的进展工夫尽管变小,但吞吐量肯定也会降落。应用该参数的实践成果:MaxGCPauseMillis 越小,单次 MinorGC 的工夫越短,MinorGC 次数增多,吞吐量升高。
  • -XX:GCTimeRatio : 吞吐量指标。不要被该参数的字面蛊惑了,它不是 GC 工夫占比的意思!它是 GC 耗时指标公式 1/(1+n) 中的 n 参数而已。GCTimeRatio 的默认值为 99,因而,GC 耗时的指标占比应为1/(1+99)=1%。应用该参数的实践成果:GCTimeRatio 越大,吞吐量越大,GC 的总耗时越小。有可能导致单次 MinorGC 耗时变长。

进展工夫越短就越适宜须要与用户交互的程序,良好的响应速度能晋升用户体验。而高吞吐量则能够高效率地利用 CPU 工夫,尽快实现程序的运算工作,次要适宜在后盾运算而不须要太多交互的工作。因而 Parallel Scavenge 适宜后盾计算为主的 Java 服务。

(2) 自适应调节内存治理

Parallel Scavenge提供了参数 -XX:+UseAdaptiveSizePolicy,它是一个开关参数,当这个参数关上之后,就不须要手工指定新生代的大小-Xmn、Eden 与 Survivor 区的比例-XX:SurvivorRatio、降职老年代对象年龄-XX:PretenureSizeThreshold 等细节参数了,虚构机会依据以后零碎的运行状况收集性能监控信息,动静调整这些参数以提供最合适的进展工夫或最大的吞吐量,这种调节形式称为 GC 自适应的调节策略GC Ergonomics

3.3.4 Serial Old 收集器

Serial Old是 Serial 收集器的老年代版本,采纳标记整顿算法,同样是单线程收集。示意如下:

Serial Old的主要用途:

  1. 供客户端模式下的老年代收集用。
  2. 服务端模式下,作为 CMS 收集失败后的 Full GC 备案,留神此时整堆都会采纳 Serail Old 做垃圾收集,而不仅仅是老年代。

另外,Serial Old底层应用的是 mark-sweep-compact 算法实现,所以有时候又叫单线程 MSC;而在 Java10 之前,G1 收集器失败时的逃生Full GC 用的是单线程的 MSC,从 Java10 开始,G1 的Full GC 改为了多线程并行执行的MSC

3.3.5 Parallel Old 收集器

Parallel Old就是 Parallel Scavenge 的老年代版本,反对 GC 多线程并行收集,基于标记整顿算法,同样关注于管制 JVM 吞吐量。其运行示意如下:

在重视 JVM 吞吐量,后盾运算较多而与用户交互较少的业务场景中,比拟适宜应用 Parallel Scavenge + Parallel Old 的组合。事实上,Parallel Old就是专门配合 Parallel Scavenge 的收集器。

例如 Java8 的服务端模式下的默认 GC 策略就是Parallel Scavenge + Parallel Old

3.3.6 CMS 收集器

目前曾经梳理的 GC 里,Serial/Serial Old/ParNew/CMS都是以缩小 STW 工夫为次要指标的。它们的实用场景个别都是互联网利用或基于浏览器的 B / S 架构的后盾服务,因为这一类的服务比拟重要的是用户交互体验,所以会很关注服务的响应速度。所以此时须要 GC 可能尽量减少 STW 的工夫。

而 CMS,就是这种场景下 HotSpot 中垃圾收集器的已经的王者。它是 HotSpot 虚拟机上第一款反对并发回收的垃圾收集器。CMS 的全称是 Concurrent Mark Sweep,即, 并发标记革除。它的运行过程相比后面几种收集器来说要简单一些,整个过程能够简略地划分为四个阶段,别离是:

  1. 初始标记,Initial mark,仅仅只标记 GC Roots 可能间接关联到的对象,STW,但速度很快;
  2. 并发标记,Concurrent mark,从间接关联对象开始遍历整个对象图,这个过程耗时较长但并不 STW,而是让用户线程和 GC 线程并发执行;
  3. 从新标记,Final remark,因为要并发标记,所以要基于增量更新的算法从新扫描 ” 由黑变灰 ” 的对象(参考后面的并发可达性剖析章节),该阶段要 STW,耗时个别比初始标记的耗时长,但也远比并发标记阶段的耗时短;
  4. 并发革除,Concurrent sweep,该阶段清理掉被标记为不可达的对象,与用户线程并发执行。

其中,初始标记与从新标记依然会 STW,但工夫都很短(从新标记在某些极其场景下会比拟耗时)。而耗时最长的并发标记和并发革除阶段,GC 线程与用户线程是并发执行的。因而,CMS 从总体上来说,它的 GC 过程和用户线程是并发执行的。如下图所示:

实际上 CMS 有 7 个阶段,这里省略了三个阶段:

  1. 在从新标记之前,其实还有 Concurrent Preclean 并发预清理阶段和 Concurrent Abortable Preclean 并发可停止预清理阶段,它们次要是为了应答可能产生的这种状况:在比拟耗时的并发标记阶段,又产生了新生代 GC,甚至可能屡次,导致有对象从新生代降职到老年代。这两个阶段是为了尽量减少这种未被可达性剖析标记过的老年代对象。也能够简略地认为它们也属于并发标记阶段。
  2. 在并发革除之后,还有一个 Concurrent Reset 并发重置阶段,该阶段将重置与 CMS 相干的数据结构,为下个周期的 GC 做好筹备。

CMS 的毛病

CMS 是 HotSpot 实现的第一款并发收集器,它的指标当然是尽量升高进展工夫。但它远远不够欠缺,至多有以下几个显著毛病:

  1. 对 CPU 资源敏感。尽管在几个并发阶段可能与用户线程并发执行,但因为占用了局部 CPU 资源,总会导致用户线程的并行数量降落,升高了零碎整体的吞吐量。CMS 默认启动的回收线程数是(处理器外围数量 +3)/4,如果 cores 数在四个以上,并发回收时垃圾收集线程只占用不超过 25% 的 CPU 资源,并且会随着 cores 数量的减少而降落。然而当 cores 数量有余四个时,CMS 对用户线程的影响就变得很大。
  2. 存在并发收集失败进而导致齐全 STW 的 Full GC 产生的危险。在并发标记和并发清理阶段,并发运行的用户程序可能会导致新的对象进入老年代,这部分对象只能等到下次 GC 再解决 (浮动垃圾),因而 CMS 不能等到老年代快满时才触发,不然在并发阶段新进入老年代的对象将无处寄存,但这个阈值个别设的比拟高(默认 90% 以上),所以会有 GC 期间老年代内存不足导致产生Concurrent Mode Failure,从而启用备用的Serial Old 收集器来进行整堆的Full GC,这样的话,STW 就很长了。
  3. CMS 采纳的是标记革除算法,会导致内存空间碎片化问题。CMS 为了解决这个问题,提供了 XX:+UseCMS-CompactAtFullCollection 开关参数和 XX:CM SFullGCsBefore-Compaction 参数,让 CMS 在执行过若干次标记革除的 GC 之后,下一次 GC 的时候先进行碎片整顿。但这样又会使进展工夫变长。
  4. JVM 参数调优绝对比拟麻烦,没有自适应调节内存治理的性能。

只管 CMS 有很多不如意的毛病,甚至在 G1 成熟后 CMS 曾经被 Oracle 摈弃 (在 Java9 中弃用并在 Java15 中正式移除)。但在目前仍然占据了大部分生产环境的 Java8 上,ParNew + CMS 仍然是大多数 Java 服务的首选 GC 策略。(只管不是默认 GC 策略)

3.3.7 Garbage First 收集器

Garbage First(简称 G1)是一款以 Region(区域)为最小内存回收单位的,逻辑上采纳分代收集实践的垃圾收集器。如果说 CMS 是已经的王者,那么 G1 就是将 CMS 扫入 ” 历史的垃圾堆 ” 的替代者。

G1 早在 Java6 中就作为试验性质的个性呈现,Java7 成为正式个性,Java8 中持续欠缺,最终在 Java9 成为 HotSpot 的首选垃圾收集器,正式取代 CMS,成为新一代的支流收集器。

从 Java9 到目前最新的 Java16,G1 始终是服务端模式下的默认垃圾收集器。当前兴许会被 ZGC 取代,But Not Today!

G1 是一个在垃圾收集器技术倒退历史上的里程碑式的成绩,它创始了收集器面向部分收集的设计思路和基于 Region 的内存布局模式。

G1 依然应用分代收集实践,但再也不是物理上将堆内存划分为间断调配的新生代和老年代,而是变成了逻辑上的,物理不间断的逻辑分代。而 Region 布局则是后续更先进的低提早垃圾收集器比方 Shenandoah 和 ZGC 都沿用或持续改善的内存布局形式。Region 布局最不言而喻的益处,就是可能更灵便不便的调配与回收内存;在内存调配回收的灵活性与内存规整性 (防止碎片化) 这两者之间,应用 Region 这种布局形式更容易获得均衡。

G1 提供了两种垃圾收集:Young GCMixed GC。这两种 GC 何时触发?如何基于 region 工作?上面先梳理一下 G1 的工作过程。

(1) G1 工作过程对内存的调配

先看一下 G1 的工作过程,以及这个过程中对内存的调配:

※1 region 分区

堆内存会被切分成为很多个固定大小区域 Region。每个Region 外部都是地址间断的虚拟内存。每个 Region 的大小能够通过 -XX:G1HeapRegionSize 参数指定,范畴是 [1M~32M],值必须是 2 的幂次方。默认会把堆内存依照2048 份均分。每个 Region 被标记了 E、S、O 和 H,这些区域在逻辑上被映射为 Eden,Survivor 和老年代(包含巨型区域 H,后续有梳理)。每个 region 并不会固定属于某个分代,而是会随着 G1 垃圾收集过程而一直变动。

JVM 某个时刻的 Region 分区对应的逻辑分代示意,如下图所示:

※2 为用户线程分配内存

刚开始的时候,大部分 Region 都是空白状态。G1 会为每个用户线程调配一个 TLAB(Thread Local Allocation Buffers)空间,可能对应有多个 TLAB 块。每个 TLAB 块的大小不会超过一个 Region,这些 TLAB 块都在 eden 的 regions 中。一个用户线程创建对象申请内存时,G1 会优先从该线程的 TLAB 块中的闲暇局部调配,如果不够再从 eden 的 regions 中申请新的 TLAB 块。这种形式的益处有二:一是在对象创立时,只用查看本人的 TLAB 块最初一个对象的前面的闲暇空间还够不够即可,从而大大放慢内存调配速度;二是使得多个用户线程的内存空间在调配时互相独立(但依然互相可见),使得多用户线程下的内存调配能够同时进行,变得无锁。

要留神的是:

  1. TLAB 块并不是 Region 外部的区域划分,它甚至不是 G1 独有的设计。它是给每个用户线程调配的固定大小不定数量的内存块(每个块小于 region 容量),尽管位于 eden Regions 之中,但只是在有用户线程运行时才会有对应的 TLAB 块被调配进来。
  2. 上图中 Region1 与 Region2 并不代表两者是地址相邻的两个 Region。G1 从 Eden 的 Regions 中调配 TLAB 时,当一个 Region 的空间都被分进来了,就会另外找个闲暇的 region 来持续分,至于这个 region 在哪,不肯定。。。

※3 触发 Young GC

G1 建设了一个 Pause Prediction Model 进展预测模型,依据冀望进展工夫来计算每次须要收集多少 Regions。当分配内存继续进行导致 eden 的 regions 数量达到这个值时,就会触发 G1 的Young GC,对所有的 eden 的 regions 进行垃圾收集。这里的算法采纳的是并行的标记复制,即,对所有 eden regions 中的对象进行可达性剖析和标记,将所有的存活对象参差地复制到闲暇的 Regions 中。这些存活对象就晋升为 Survivor 对象,所属的 Region 则从闲暇状态变为 S 状态(Survivor),原先 eden regions 会被通通清空从新回到闲暇状态。

相似 Serial 等经典的新生代收集器对新生代的比例划分,G1 对 Survivor 的 Regions 数量也有 --XX:TargetSurvivorRatio 来管制,默认也是8:1。逻辑上来讲,G1 的 young GC 也是一个 eden 两个 survivor 进行复制的。

同时,Young GC还负责其余的一些数据统计工作,比方保护对象年龄相干信息,即存活对象经验过 Young GC 的总次数。

很显然,G1 的 Young GC 是 STW 的,但基于分代收集实践咱们晓得对新生代的可达性剖析理论可能可达的对象很少,所以进展工夫很短暂。

※4 降职老年代

JVM 通过多轮 young GC 之后,那些总能存活下来的对象就会降职到老年代。对于 G1 来说,就是在 young GC 时,将满足降职条件的对象从 survivor region 复制到老年代 region 中。如果须要应用闲暇 region 来寄存降职的对象,那么这个闲暇 region 就变为了老年代 region。

这里有个概念,叫Promotion Local Allocation Buffers,即PLAB,相似于TLAB。但 PLAB 不是给用户线程调配的,是给 GC 线程调配的。当对象降职到 survivor 分区或者老年代分区时,复制对象所需内存是从每个 GC 线程本人的 PLAB 空间调配的。应用 PLAB 的益处与 TLAB 雷同,更有效率且多线程无锁。

※5 混合收集

当老年代空间在整个堆空间中占比 (IHOP) 达到一个阈值 (默认 45%) 时,G1 会进入 并发标记周期 (前面有梳理),而后会进入 混合收集周期 。收集的指标是 整个新生代的 regions + 老年代中回收价值较高的 regions。相当于 新生代收集 + 局部老年代收集

G1 每次收集,会将须要收集的 region 放入一个叫 CSet 的汇合,联合下图来了解 young GCMixed GC的收集对象不不同:

所谓回收价值,是 G1 对每个老年代 region 做出的基于回收空间 / 回收耗时及冀望暂停工夫的价值评估。G1 在进行混合回收时,并不是对所有老年代 region 都进行回收,而是依据这个回收价值,选取价值较高的那些老年代进行收集。

这里就能看到为什么叫Garbage First,因为会首先收集垃圾比拟多的 Region。。。

混合收集中的老年代收集和新生代收集一样,采纳的是也是并行的复制算法。

※6 巨型区域(Humongous Region)

Region 中还有一类非凡的 Humongous Region,专门用来存储巨型对象。G1 认为只有大小超过了一个 Region 容量一半的对象即可断定为巨型对象。而对于那些超过了整个 Region 容量的巨型对象,将会被寄存在 N 个间断的Humongous Region 之中,G1 的大多数行为都把 Humongous Region 作为老年代的一部分来进行对待。

至此,大抵梳理了 G1 如何治理内存的问题。接下来,持续梳理 G1 的残缺的流动时序。

(2) G1 垃圾收集流动时序

G1 垃圾收集的残缺周期比较复杂,参考上面的 G1 垃圾收集流动时序图:

整个 G1 的垃圾收集流动,大抵能够分为以下几个期间:

  • 用户线程运行期间,此时 G1 只有 RSet 保护线程在与用户线程同时运行。RSet 是 Region 之间用于保留相互之间对象援用关系的记忆集,每个 region 都有一个 RSet。每个 RSet 记录的是哪些 Region 对本人的 Region 中的对象存在援用关系。其构造是一个 hashtable,key 是其余 Region 的起始地址,value 是一个汇合,外面的元素是其余 Region 中的 Card 的 index。Card 是 Region 外部的堆内存最小可用粒度,512 字节,有一个 Global Card Table 全局卡片表用来记录所有 Region 的 Card。对象会调配在单个或物理间断的多个 Card 上。如果 Region1 的对象 A 援用了 Region2 的对象 B,而对象 A 的起始地址位于 Region1 的某个 card,那么在 Region2 的 RSet 中就会有 Region1 的记录,key 是 Region1 的起始地址,value 汇合中蕴含 Region1 对应 Card 在全局卡片表中的 Index。记忆集次要用于解决可达性剖析中的跨 Region 援用的问题,利用 Card 技术能够疾速定位到对象。
  • young GC期间,后面曾经梳理,这里不再赘述。
  • 并发标记周期。当老年代的整堆空间占比 (IHOP) 达到阈值 (45%) 时,触发并发标记周期。这个周期看起来很像 CMS。它有以下几个阶段:初始标记,收集所有 GC 根及其间接援用,这一阶段是与 young GC 一起实现的,即图中的Young GC with Initial Mark;并发标记,标记存活对象,并发标记期间可能又会触发young GC,相似 CMS 中的对应阶段;从新标记,同样相似于 CMS 中的对应阶段,是为了解决并发标记导致的 ” 由黑变灰 ” 问题,但 CMS 应用的是增量更新算法,而 G1 用的是原始快照算法;革除阶段,辨认回收价值较高的老年代 Region 并退出 CSet,并间接回收曾经没有任何存活对象的 Region 使之回到闲暇 Region 状态。从新标记与革除阶段都是并行执行的。
  • 混合收集周期,在并发标记周期完结之后,会开始混合收集周期。有两点要留神。第一是混合收集并不会马上开始,而是会先做至多一次 youngGC,因为后面并发标记周期的革除阶段可能曾经革除了不少齐全没有存活对象的 Region,此时不用焦急回收曾经进入 CSet 的那些回收价值较高的老年代 Region。第二,混合收集并不是一次性回收 CSet 中所有 region,而是分批收集。每次的收集可能有新生代 region 的收集,可能有老年代 region 的收集。具体采纳的算法曾经在后面叙述过,这里不再赘述。混合收集次数能够通过不同的 JVM 参数配合管制,其中比拟重要的有:-XX:G1MixedGCCountTarget,指定次数指标;-XX:G1HeapWastePercent,每次混合收集后计算该值,达到指定值后就不再启动新的混合收集。

1. 对于记忆集与卡表,其实这种设计并非 G1 独有,所有的基于分代收集实践的收集器在解决跨代援用的问题时,都须要应用记忆集与卡表技术。只是 G1 的 RSet 与卡表的设计相对而言更简单。G1 之前的分代收集器只用思考新生代和老年代,比方新生代收集时,对应老年代有个卡表,但凡有跨代援用的对象,其卡表中的元素的值会被标记为 1,成为变脏,垃圾收集时通过扫描卡表里变脏的元素就能得出蕴含跨代援用的 Card,将其中的对象退出 GcRoots 即可。G1 则如前所述,因为 Region 的数量远超出分代数量,因而给每个 Region 设计了一个 RSet,同时还有一个全局卡表。

2. 对于 增量更新 原始快照,为什么 G1 用 SATB?CMS 用增量更新?

SATB 绝对增量更新效率会高(当然 SATB 可能造成更多的浮动垃圾),G1 因为 region 数量远多于 CMS 的分代数量(CMS 就一块老年代区域),从新深度扫描增量援用的根对象的话,G1 的代价会比 CMS 高得多,所以 G1 抉择 SATB,等到下一轮 GC 再从新扫描。

(3) G1 的垃圾收集担保

G1 在对象复制 / 转移失败或者没法调配足够内存(比方巨型对象没有足够的间断分区调配)时,会触发 Full GC,应用的是与Serial Old 收集器雷同的 MSC 算法实现对整堆进行收集。所以一旦触发 Full GC 则会 STW 较长时间,执行效率很低。

Java10 之前是单线程MSC,Java10 中改良为多线程MSC

(4) G1 比照 CMS

相比 CMS,G1 有以下劣势:

  • Pause Prediction Model进展预测模型,反对指定在一个长度为 M 毫秒的工夫片段内,耗费在垃圾收集上的工夫大概率不超过 N 毫秒这样的指标。用户能够设定整个 GC 过程的冀望进展工夫,参数 -XX:MaxGCPauseMillis 指定一个 G1 收集过程指标进展工夫,默认值200ms,不过它不是硬性条件,只是期望值。G1 是通过进展预测模型计算出来的历史数据来预测本次收集须要抉择的 Region 数量,从而尽量满足用户设定的指标进展工夫。
  • 基于 Region 的内存布局,使得内存调配与回收更加灵便。
  • 按回收价值动静确定回收集,使得老年代的回收更加高效。
  • 与 CMS 的“标记 - 革除”算法不同,G1 从整体来看是基于“标记 - 整顿”算法实现的收集器,但从部分 (两个 Region 之间) 上看又是基于“标记 - 复制”算法实现。无论如何,这两种算法都意味着 G1 运作期间产生内存空间碎片要少得多,垃圾收集实现之后能提供更多规整的可用内存。这种个性有利于程序长时间运行,在程序为大对象分配内存时不容易因无奈找到间断内存空间而提前触发下一次 GC。

从工作时序上看,CMS 在对老年代的回收上,采纳了革除算法能够并发执行,而 G1 的老年代回收采纳整顿算法,会导致 STW。仿佛 CMS 的进展应该少一点。从设计理念上说,采纳整顿算法的 G1 的确应该是更重视吞吐量而非低提早的。但因为 G1 采纳了很多新的设计思路,特地是进展预测模型、Region 与回收价值,导致实际上 G1 很容易做到比 CMS 更低的进展,特地是内存短缺的场景。

相比 CMS,G1 的劣势是,无论是内存耗费还是 CPU 负载,G1 都比 CMS 要耗费更多的资源。

例如,因为 Region 分区远比新生代老年代的分区数量多,且 RSet 保护老本更高,导致用户线程运行期间 G1 的 RSet 保护线程要耗费更多的资源。G1 的记忆集 (和其余内存耗费) 可能会占整个堆容量的 20% 乃至更多的内存空间,这要比 CMS 多得多。

综上,目前在小内存 (4G 以下) 利用上,较大概率依然是 CMS 体现更好;而当内存达到 6G 或 8G 以上的时候,G1 的劣势就显著起来了。但随着 G1 越来越成熟,尤其是 Java9 当前,无脑应用 G1 是一个没啥大问题的抉择。

事实上,Oracle 对 G1 的定位就是Fully-Featured Garbage Collector,全功能的垃圾收集器。

3.3.8 经典垃圾收集器小结

对经典垃圾收集器做一个小结,比照如下:

收集器 执行形式 新生代 or 老年代 算法 关注点 实用场景
Serial 串行 新生代 标记复制 响应速度优先 单 CPU 环境下的 Client 模式
Serial Old 串行 老年代 标记整顿 响应速度优先 单 CPU 环境下的 Client 模式,CMS 与 G1 的后备计划
ParNew 并行 新生代 标记复制 响应速度优先 多 CPU 环境时在 Server 模式下与 CMS 配合
Parallel Scavenge 并行 新生代 标记复制 吞吐量优先 在后盾运算而不须要太多交互的工作
Parallel Old 并行 老年代 标记整顿 吞吐量优先 在后盾运算而不须要太多交互的工作,与 Parallel Scavenge 配合
CMS 并发 老年代 标记革除 响应速度优先 集中在互联网站或 B / S 零碎的与用户交互较多的服务端上的 Java 利用
G1 并发 两者 标记 - 整顿 + 复制 响应速度优先 面向服务端利用,用来代替 CMS

四、低提早垃圾收集器

掂量一款垃圾收集器有三项最重要的指标:

  • 内存占用(Footprint)
  • 吞吐量(throughput)
  • 提早(Latency)

这三者大概是一个不可能三角,一款优良的收集器通常能够在其中一到两项上做到很好。而随着硬件的倒退,这三者中,提早的重要性越来越凸显进去。起因有三:一是随着内存越来越大还越来越便宜,咱们越来越能容忍收集器占用多一点的内存;二是硬件性能的增长,比方更快的 CPU 处理速度,这对软件系统的解决能力是有间接晋升的,它有助于升高收集器运行时对应用程序的影响,换句话说,JVM 吞吐量会更高;三,与前两者相同,有些硬件晋升,特地是内存容量的晋升,并不会间接升高提早,相同,它会带来负面成果,更多的内存回收必然使得回收耗时更长。

因而,在当下,垃圾收集器的次要指标就是低提早。对于 HotSpot 而言,目前有两款转正不久的低提早垃圾收集器:ShenandoahZGC。它们在低提早方面与之前梳理过得经典垃圾收集器比拟如下:

从上图能够看出垃圾回收器的发展趋势:

  1. 尽量减少并发,以缩小 STW。G1 指标是 200ms,但理论只能做到四五百 ms 高低,Shenandoah 目前能够做到几十 ms 以内,ZGC 就牛逼了,10ms 以内。
  2. 尽量减少空间碎片,以保障吞吐量。少用标记革除算法,事实上除了 CMS 也没谁用。

除了 Parallel,从CMSG1,再到 ShenandoahZGC,都是在想方法并发地实现标记与回收,以达到升高提早的目标。同时为了尽可能保障吞吐量,在回收阶段也尽量应用整顿算法而不是革除算法。

继 G1 之后,ShenandoahZGC 的指标就是,在尽可能对吞吐量影响不太大的前提下,实现任意堆内存大小下都能够把垃圾收集的进展工夫限度在十毫秒以内的低提早。即:大内存,低提早。

ShenandoahZGC 在 Java15 中都曾经成为正式个性 (但默认 GC 还是 G1),上面简略梳理一下ShenandoahZGC的特点,它们的要害都在于,如何实现并发整顿。

4.1 Shenandoah 收集器

Shenandoah 收集器在技术上能够认为是 G1 的下一代继承者。但它不是由 Oracle 主推倒退的收集器,它是由 RedHat 公司主推的,属于 OpenJDK 的个性,而非 OralceJDK 的个性。它在 OpenJDK12 中引入成为试验个性,在 OpenJDK15 中成为正式个性。

Shenandoah 与 G1 一样,应用基于 Region 的堆内存布局,有用于巨型对象存储的Humongous Region,有基于回收价值的回收策略,在初始标记、并发标记等阶段的解决思路上高度一致,甚至间接共享了一部分实现代码。这使得 G1 的一些改善会同时反映到 Shenandoah 上,Shenandoah 的一些新个性也会呈现在 G1 中。例如 G1 的收集担保Full GC,以前是单线程的MSC,就是因为合并了 Shenandoah 的代码,才变为并行的多线程MSC

Shenandoah 相比 G1,次要有以下改良:

  1. 回收阶段反对并发整顿算法;
  2. 不反对分代收集实践,不再将 Region 辨别为新生代和老年代;
  3. 不应用 RSet 记忆集,改为全局的连贯矩阵,连贯矩阵就是一个二维表,RegionN 有对象援用 ReginM 的对象,就在二维表的 N 行 M 列上打钩。

在大的流程上,因为不再基于分代收集实践,Shenandoah 并没有所谓 YoungGCOldGCMixedGC。它的次要流程相似 G1 的并发标记周期和混合收集周期:

  • 初始标记:标记与 GCRoots 间接关联的对象,短暂 STW。
  • 并发标记:与 G1 相似,并发执行,标记可达对象。
  • 从新标记:应用 SATB 原始快照算法从新标记,并统计出各个 Region 的回收价值,将最高的 Regions 组成一个 CSet,短暂 STW。
  • 并发清理:将没有任何存活对象的 Region 间接清空。
  • 并发回收:从这里开始,就是 Shenandoah 和 G1 的要害差别,Shenandoah 先把回收集外面的存活对象先复制一份到其余未被应用的 Region 中,并利用转发指针、CAS 与内存屏障等技术来保障并发运行的用户线程能同时放弃对挪动对象的拜访(※1)。
  • 初始援用更新:并发回收阶段复制对象完结后,须要把堆中所有指向旧对象的援用修改到复制后的新地址,这个操作称为援用更新。初始援用更新 这个阶段实际上并未做具体的更新解决,而是建设一个线程汇合的工夫点,确保所有并发回收阶段中的 GC 线程都已实现调配给它们的对象复制工作而已。初始援用更新工夫很短, 会产生一个十分短暂的 STW。
  • 并发援用更新:开始并发执行援用更新,找到对象援用链中的援用所在,将旧的援用地址改为新的援用地址。
  • 最终援用更新:更新 GCRoots 中的援用。
  • 并发清理:回收 CSet 中的 Regions。

※1 实现并发整顿的关键技术:转发指针、CAS 与读写屏障

转发指针,Brooks PointerBrooks是一个人的名字,转发指针技术由其提出。该技术在原有对象布局构造的最后面对立减少一个新的援用字段, 在失常不处于并发挪动的状况下, 该援用指向对象本人;当对象被复制,有了一份新的正本时,只须要批改旧对象上转发指针的援用地位,使其指向新对象,便可将所有对该对象的拜访转发到新的正本上。这样只有旧对象的内存依然存在,未被清理掉,虚拟机内存中所有通过旧援用地址拜访的代码便依然可用,都会被主动转发到新对象上持续工作。而当援用地址全副被更新之后,旧对象就不会再被拜访到,转发指针不再由永无之地,随着旧对象一起被开释。

当然,应用转发指针会有线程平安问题。比方 GC 线程复制对象用户线程对旧对象执行写操作 这两个动作如果同时产生,就有可能呈现线程平安问题。Shenandoah 在这里是采纳Compare And SwapCAS 技术来保障线程平安的。

CAS 是一种乐观锁,比拟并替换。

另外,在对象被拜访时触发转发指针动作,须要应用读写屏障技术,在对象的各种读写操作 (包含读,写,比拟,hash,加锁等等) 上做一个拦挡,相似 AOP。

留神,这里的读写屏障并不是 JMM 中的内存屏障。内存屏障相似同步锁,是多线程环境下工作线程与主存之间保障共享变量的线程平安的技术。

事实上,在之前介绍的其余收集器中,曾经利用到了写屏障技术,比方 CMS 与 G1 中的并发标记等。Shenandoah 不仅要用写屏障,还要用读屏障,这是它之前的性能瓶颈之一,但在 Java13 中失去了改善,改为应用Load Reference Barrier,援用拜访屏障,只拦挡对象中数据类型为援用类型的读写操作,而不去管原生数据类型等其余非援用字段的读写,这可能省去大量对原生类型、对象比拟、对象加锁等场景中设置屏障所带来的耗费。

目前,Shenandoah 在进展工夫上与 G1 等经典收集器相比有了质的飞跃,曾经可能做到几十毫秒;但一方面还没有达到预期的 10ms 以内,另一方面却引起了吞吐量的显著降落,尤其是和 Parallel Scavenge 相比。

4.2 ZGC 收集器

Z Garbage Collector,简称 ZGC。ZGC 是 Oracle 主推的下一代低提早垃圾收集器,它在 Java11 中退出 JVM 作为试验个性,在 Java15 中成为正式个性。目前还不是默认 GC,但很有可能成为继 G1 之后的 HotSpot 默认垃圾回收器。

ZGC 尽管指标和 Shenandoah 一样,都是把进展工夫管制在 10ms 以内,但它们的思路却齐全不一样。就目前而言,ZGC 根本曾经做到了,而 Shenandoah 还须要持续改善。

ZGC 收集器是一款基于 Region 内存布局的,(临时)不设分代的,应用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记 - 整顿算法的,以低提早为首要指标的一款垃圾收集器。其次要特点如下:

  1. 动静的 Region 布局
  2. 回收阶段反对更奇妙的并发整顿

1. 内存布局

ZGC 的 Region 是动态创建和销毁的,且大小不是全副雷同的。在 X64 平台下,ZGC 的 Region 有大、中、小三个容量:

  • 小型 Region,容量固定为 2M,用于寄存小于 256K 的小对象。
  • 中型 Region,容量固定为 32M,用于寄存 [256K~4M) 的对象。
  • 大型 Region,容量不固定,但必须是 2M 的整数倍,用于寄存 4M 及以上的大对象。每个大型 Region 只会寄存一个大对象,这意味着会有小于中型 Region 的大型 Region,比方最小的大型 Region 只有 4M,就比 32M 的中型 Region 小。大型 Region 在 ZGC 中是不会被挪动的。

2. 更奇妙的并发整顿

ZGC 的运行阶段大抵如下(初始标记什么的就不写了):

  • 并发标记:对所有 Regions 做并发的可达性剖析,但标记的后果记在援用地址的固定地位上。这个技术叫染色指针技术(※1)。
  • 并发准备充沛配:依据标记后果将所有存活对象所属的 Region 计入重调配集Relocation Set
  • 并发重调配:并发地将重调配集中的存活对象复制到闲暇 Region 中,并未重调配集中的每一个 Region 保护一个转发表 Forward Table,记录旧对象到新对象的转发关系。因为染色指针技术的应用,ZGC 仅仅从援用上就能获知该对象是否在重调配集中(存活对象),通过预置好的读写屏障,如果用户线程并发拜访了该对象,就会被截获并依据转发表转发到复制的新对象上,同时主动修改援用地址到新对象地址,这种主动批改援用地址的行为被称为 自愈。ZGC 这种设计的益处是,转发只会产生一次,并发期间用户线程再次拜访该对象就没有转发的损耗了,不像 Shenandoah 的转发指针技术,在并发期间用户线程每次拜访该对象都要转发一下。另外的益处是,因为染色指针技术,重调配集中的存活对象只有被复制完,就能够立刻清空这个 Region,只有保留其对应转发表即可。
  • 并发重映射:修改整个堆中对重调配集中旧对象的所有援用。这个阶段并不是一个迫切要实现的阶段,因为在 ZGC 的设立里,援用能够 ” 自愈 ”。但这么做还是有益处:开释转发表。因而 ZGC 抉择将并发重映射这一步骤放到下一次 GC 的并发标记阶段去实现,这样省下了一次遍历援用链的过程。

※1 染色指针技术

染色指针技术 Colored Pointer 是 ZGC 的标志性设计。个别收集器在可达性剖析标记对象的三色状态时,都是标记在对象或与对象相干的数据结构上。而染色指针技术是将三色状态间接标记到援用这个 ” 指针 ”,或者说内存地址上。以 64 位的 linux 为例,它反对的内存地址空间去掉保留的高位 18 位不能应用,还剩下 46 位。ZGC 将这剩下的 46 位中的高 4 位拿进去存储 4 个标记信息,包含三色状态,对象是否曾经被复制,是否只能通过 finalize 办法拜访。

染色指针技术要间接批改操作系统的内存地址,这是须要操作系统和 CPU 的反对的。在 x86-64 平台上,就须要利用到虚拟内存多重映射技术了。

染色指针技术带来的收益:

  1. 一旦重调配集中的某个 Region 的存活对象全副复制完结后,该 Region 可能立刻清空,马上就能够拿来调配新对象。这使得 ZGC 能够在闲暇 Region 极少的极其状况下仍然保障可能实现回收。
  2. 大幅缩小在对象上设置读写屏障导致的性能损耗。因为能够间接从指针读到三色标记,是否已被复制等信息。这使得 GC 对用户线程的性能影响减低,即,缩小了 ZGC 对吞吐量的影响。
  3. 染色指针是一种能够扩大的技术,比方当初不能应用的高位 18 位,如果开发了这 18 位,ZGC 就不用强占目前的 46 位,从而扩充反对的堆内存容量,也能够记录一些其余的标记信息。

染色指针技术的劣势:

  1. 不反对 32 位操作系统,因为没有地址空间不够。
  2. 可能治理的内存不能超过 2 的 42 次幂,即 4TB。目前来说,倒是齐全够用。大内存的 Java 利用有上百 G 就不得了了。与这点限度相比,它能带来的收益要大得多。

目前 ZGC 的劣势还是很显著的,进展工夫方面,曾经做到了 10ms 以内,而在 ” 弱势 ” 的吞吐量方面,竟然也曾经根本追平以吞吐量为指标的Parallel Scavenge。基本上能够认为是齐全超过 G1 的。但 ZGC 也有须要衡量的中央,ZGC 没有分代,不能针对新生代那种 ” 朝生夕灭 ” 的对象做针对性的优化,这导致 ZGC 可能接受的内存调配速度不会太高。即,在一个会间断的高速的大量的分配内存的场景下,ZGC 每次收集周期都会产生大量浮动垃圾,当回收速度跟不上浮动垃圾产生的速度时,堆中的残余空间会越来越少,最初可能导致收集失败。

其实从 CMS 到 G1,以及 Shenandoah 都有浮动垃圾的问题。但前两者的分代设计根本保障浮动垃圾不会太多,Shenandoah 其实也有相似 YoungGC 的阶段设计去解决大量的新生对象。

五、其余垃圾收集器

除了以上梳理的 7 种经典垃圾收集器和两种低提早垃圾收集器,还有一些其余的 GC:

  1. Java11 减少了一个叫 Epsilon 的收集器。它的特点就是对内存的治理是只调配,不回收。用处之一是用于须要剥离垃圾收集器影响的性能与压力测试;另外的用处是那些运行负载极小不须要任何回收的小利用,比方 Function 服务,几秒就执行完的脚本,特点就是跑完就敞开 JVM。
  2. Azul 的 Pauseless GC,简称PGC,和Concurrent Continuously Compacting Collector,简称C4。它们大概相当于 ZGC 的同胞前辈,都是商用 VM 的 GC,早就做到了标记和整顿的全程并发。PGC 运行在Azul VM 上,C4 运行在 Zing VM 上,且 C4 反对分代。从技术上讲,PGC、C4、ZGC 一脉相承。
  3. OpenJDK 当初除了 HotSpot 虚拟机,还反对 OpenJ9 虚拟机,它有本人的垃圾收集器如 ScavengerConcurrent MarkIncremental Generational 等等,不理解。

六、垃圾收集器的抉择及相干参数

在理论生产环境上应该如何抉择垃圾收集器?相干参数如何设置?

这里只思考 OpenJDK/OracleJDK 的 HotSpot 虚拟机上的垃圾收集器。没有将 Azul 和 OpenJ9 的垃圾收集器退出抉择范畴,因为不理解。

6.1 收集器的抉择

思考垃圾收集器的抉择时,须要思考以下事项:

  1. 对于不可能三角(内存占用 / 吞吐量 / 提早),利用的次要性能更关注什么?比方一个 OLAP 零碎,它的大部分性能是各种数据分析和运算,指标是尽快实现计算工作给出后果,那么吞吐量就是次要关注点。而一个 OLTP 零碎,它须要与用户频繁交互,用户频繁地通过页面操作录入数据,那么提早就是次要关注点。而如果是一个客户端利用或者嵌入式应用,那么内存占用就是次要关注点。
  2. 利用运行的基础设施如何?包含但不限于 CPU 核数,内存大小?操作系统是 Linux 还是 Windows 等等。
  3. 应用的 JDK 是什么版本?9 之前还是 9 当前?

简略列一个表格,能够参考该表格抉择垃圾收集器:

垃圾收集器 关注点 硬件资源 操作系统 JDK 版本 劣势 劣势
ZGC 低提早 大内存,很多核 64 位 Linux 等 JDK15 以上 提早真低 不反对 windows,须要足够的 JVM 调试能力
Shenandoah 低提早 大内存,很多核 openJDK15 以上 提早很低 须要足够的 JVM 调试能力
G1 低提早 4G 以上内存,多核 JDK9 以上 提早绝对较低,技术成熟 内存占用绝对较多
ParNew+CMS 低提早 4G 以下内存,多核 JDK9 之前 提早绝对较低,技术成熟 曾经被摈弃,更大的堆内存比不上 G1
Parallel(Scavenge+Old) 吞吐量 吞吐量高 提早较高
Serial+Serail Old 内存占用 内存占用低,CPU 负荷低 提早高,吞吐量低

简略总结一下就是,支流的,就做一个一般的 JavaWeb,或者微服务中的后盾服务,与用户交互较多,那就依据 Java 版本来,不是 G1 就是ParNew+CMS

  • Java 版本在 9 及 9 以上,那就无脑抉择 G1,默认即可;
  • Java 版本还是 8 甚至更老,那就 ParNew+CMS;

有点谋求的,依据状况来:

  • 有足够的 JVM 调优能力,且对低提早有谋求,能够尝试 ZGC 或 Shenandoah;
  • 次要做后盾单纯数据运算的,比方 OLAP,能够尝试Parallel(Scavenge+Old)
  • C/ S 架构的客户端利用,或者嵌入式应用,能够尝试Serial+Serail Old

6.2 GC 日志参数

Java9 之前,HotSpot 没有对立日志框架,参数比拟凌乱;Java9 之后,所有日志性能都归纳到了 -Xlog 参数上。

Java9 的 JVM 日志应用形式:

-Xlog[:[selector][:[output][:[decorators][:output-options]]]]

阐明:

  • selector:选择器,由标签 tag 和日志级别 level 组成。标签是功能模块,它指定输入哪个功能模块的日志,GC 日志的话,标签就是gc;日志级别从低到高有:TraceDebugInfoWarningErrorOff
  • decorators:润饰器,指定每行日志的额定输入内容,包含:time,以后工夫戳;uptime,VM 启动到当初的秒数;timemillis,以后工夫毫秒数;uptimemillis,VM 启动到当初的毫秒数;timenanos,以后工夫纳秒数;uptimenanos,VM 启动到当初的纳秒数;pid,过程 ID;tid,线程 ID;level,日志级别;tags,日志标签。默认是 uptimeleveltags

参考:Java9 前后 GC 相干日志参数比照

性能 Java9 及之后 Java9 之前
查看 GC 根本信息 -Xlog:gc -XX:+PrintGC
查看 GC 详细信息 -X-log:gc* -XX:+PrintGCDetails
查看 GC 前后空间变动 -Xlog:gc+heap=debug -XX:+PrintHeapAtGC
查看 GC 中并发工夫和进展工夫 -Xlog:safepoint -XX:+Print-GCApplicationConcurrentTime-XX:+PrintGCApplicationStoppedTime
查看 GC 自适应调节信息 -Xlog:gc+ergo*=trace -XX:+PrintAdaptive-SizePolicy
查看 GC 后残余对象年龄散布 -Xlog:gc+age=trace -XX:+PrintTenuring-Distribution

Java8 的 GC 日志的查看,请参考以前的文章:

java8 增加并查看 GC 日志(ParNew+CMS)

6.3 G1 相干 JVM 参数配置

G1 与 Parallel Scavenge 一样,反对自适应调节的内存治理,所以 G1 调优绝对简略,指定指标进展工夫之后,尽量避免呈现 Full GC 即可。

所以首先,不要在代码里呈现 System.gc() 这么险恶的显式申请 GC。

该办法会申请 JVM 做一次 Full GC!而不是 G1 的 youngGC 或 MixedGC!尽管 JVM 不肯定许可,许可了也不晓得啥时候做。但这么做很大概率会导致 JVM 多做一次Full GC!要记住,Full GC 是全程 STW 的,它是升高 JVM 性能体现的第一杀手!除非你的 application 的次要是利用堆外的间接内存 (Java 的 NewIO 提供的性能) 做一些相似大文件拷贝的业务,否则不要应用System.gc()

能够应用 -XX:+DisableExplicitGC 禁止显示调用 GC。

不思考 System.gc() 的状况下,G1 呈现 Full GC 次要是因为因为降职或大对象导致老年代空间不够了。所以 G1 调优的次要方向包含:

  • 减少堆大小,或适当调整老年代和年老代比例。这样能够间接减少老年代空间,内存回收领有更大的余地。
  • 适当减少并发周期线程数量,充分发挥多核 CPU 性能。目标是缩小并发周期执行工夫,从而放慢回收速度。
  • 让并发周期尽早开始,更改 IHOP 阈值(默认 45%)。
  • 在混合收集周期回收更多老年代 Region。

G1 罕用参数:

  • -XX:+UseG1GC:应用 G1 收集器,Java9 后默认应用。
  • -XX:MaxGCPauseMillis=200:指定进展冀望工夫,默认 200ms。该值是期望值,G1 会依据该值主动调整内存治理的相干参数。
  • -XX:InitiatingHeapOccupancyPercent=45:IHOP 阈值,老年代 Regions 数量达到该值时,触发并发标记周期。
  • -XX:G1MixedGCLiveThresholdPercent=n:如果一个 Region 中的存活对象比例超过该值,就不会被筛选为垃圾分区。
  • -XX:G1HeapWastePercent:混合收集周期中,每次混合收集后计算该值,可回收比例小于该值后就不再启动新的混合收集。
  • -XX:G1MixedGCCountTarget=n:混合收集周期次数目标值,默认 8。
  • -XX:G1OldCSetRegionThresholdPercent:混合收集周期中,每次混合收集的最大老年代 Regions 数量,默认 10。
  • -XX:NewRatio=n:老年代 / 新生代的 Regions 比例,不要设置这个参数 ,否则进展冀望工夫将生效,G1 最大的劣势 进展预测模型 将进行工作。
  • -XX:SurvivorRatio=n:新生代中 eden 与 survivor 区域的比例,默认 8,即 8 个 eden 区域对应 2 个 survivor 区域。
  • -XX:MaxTenuringThreshold =n:新生代降职老年代的年龄阈值,默认 15。
  • -XX:ParallelGCThreads=n:并行收集时的 GC 线程数,不同平台默认值不同。
  • -XX:ConcGCThreads=n:并发标记时的 GC 线程数,默认是 ParallelGCThreads 的四分之一。
  • -XX:G1ReservePercent=n:堆内存的预留空间百分比,默认 10,即默认地会将 10% 的堆内存预留下来,用于升高老年代空间有余的危险。
  • -XX:G1HeapRegionSize=n:单个 Region 大小,取值区域为[1M~32M],必须是 2 的次幂。默认依据堆空间大小主动计算,切成 2048 个。

G1 调优倡议:

  1. 不要本人显式设置新生代的大小(-Xmn-XX:NewRatio),如果显式设置新生代的大小,会导致进展冀望工夫这个参数生效。
  2. 因为 进展预测模型 的存在,调优时应该首先调整 -XX:MaxGCPauseMillis 参数来让 G1 主动调整达到目标,其余参数先不要手动设置。调整冀望工夫次要是找到平衡点:太大当然不行,间接减少了进展工夫;但太小的话,则意味着新生代变小,youngGC 频率回升,也会减小混合收集周期中每次混合收集的 Region 数量,可能反而会导致老年代不能尽快回收从而产生FullGC。指定该值时,应该作为 90% 期望值,而不是平均值。
  3. 如果再怎么调整 -XX:MaxGCPauseMillis 参数都还是有 Full GC 产生,那么能够尝试手动调整:
  • 适当减少 -XX:ConcGCThreads=n 并发标记时的 GC 线程数,目标是放慢并发标记速度,不能减少太多,会影响用户线程执行,升高吞吐量。
  • 适当升高-XX:InitiatingHeapOccupancyPercent=45,适当升高该值能够提前触发并发标记周期,从而肯定水平上防止老年代空间有余导致的Full GC。但这个值如果设置得过小,又会导致 G1 频繁得进行并发标记与混合收集,会减少 CPU 负荷,升高吞吐量。通过 GC 日志能够判断该值是否适合:在一轮并发周期完结后,须要确保堆的闲暇 Region 的比例小于该值。
  • 调整 G1 垃圾收集器的混合收集的工作量,即在一次混合垃圾收集中尽量多解决一些 Region,能够从另外一方面进步混合垃圾收集的效率。例如:适当调大-XX:G1MixedGCLiveThresholdPercent=n,这个参数的值越大,某个 Region 越容易被辨认为回收价值高;适当减小-XX:G1MixedGCCountTarget=n,减小这个值,能够减少每次混合收集的 Region 数量,然而可能会导致进展工夫过长;

更多参数调优请参考资料:

Garbage First Garbage Collector Tuning

6.4 CMS 相干 JVM 参数配置

CMS 根本已被淘汰,这里给出一些生产上罕用的参数:

-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:+CMSScavengeBeforeRemark

其中:

  • -XX:+UseParNewGC : 指定新生代应用 ParNew 垃圾回收器。
  • -XX:+UseConcMarkSweepGC : 指定老年代应用 CMS 垃圾回收器。
  • -XX:+UseCMSInitiatingOccupancyOnly:应用设定的 CMSInitiatingOccupancyFraction 阈值。
  • -XX:CMSInitiatingOccupancyFraction : CMS 触发阈值,当老年代内存应用达到这个比例时就触发 CMS。
  • -XX:+CMSParallelRemarkEnabled : 让 CMS 采纳并行标记的形式升高进展。
  • -XX:+CMSScavengeBeforeRemark:在 CMS GC 前先启动一次 youngGC,目标在于缩小跨代援用,升高从新标记时的开销
正文完
 0