GC算法与收集器
# 垃圾收集算法
# 标记-清除算法(Mark-Sweep)
标记-清除算法(Mark-Sweep)是最基础的垃圾回收算法,之所以说它是最基础的是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:

从图中可以很容易看出标记-清除算法实现起来比较容易,它的主要不足有两个:
- 标记和清除两个过程的效率都不高;
- 标记清除之后会产生大量不连续的内存碎片。空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作;
标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。扫描了整个空间两次,因此效率比较低。如下图所示。

标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
# 复制算法(Copying)
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。如下图所示:

这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。
很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。所以比较适用于年轻代:基本上98%的对象是"朝生夕死"的,存活下来的会很少。
复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时把堆分成 一个对象面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集合(GC Roots)中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存:

# 标记-整理算法(Mark-Compact)
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是让所有存活的对象都向一端移动,然后直接淸理掉端边界以外的内存。如下图所示:

标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。具体流程见下图:

# 分代收集策略(Generational Collection)
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),这样就可以根据各个年代的特点采用最适当的收集箅法。

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,因此采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间(一般称为S0和S1,或者是from和to),且Eden:S0:S1=8:1:1。每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
而老年代中对象的存活率高、每次回收都只回收少量对象,因此使用[标记-清除]或[标记-整理]算法(主要是标记-整理算法)。
# Minor GC、Major GC、Full GC
我们再来看一下Java堆的内存分布图:

# Minor GC
所有新生成的对象首先都是放在年轻代。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象,因此采用复制算法。当年轻代内存空间被用完时,就会触发垃圾回收。这个垃圾回收叫做Minor GC。
新生代内存按照8:1:1的比例分为一个eden区和两个survivor(s0,s1)区。大部分对象在Eden区中生成。回收时先将Eden区存活对象复制到一个S0区,然后清空Eden区,当这个S0区也存放满了时,则将eden区和S0区存活对象复制到另一个s1区,然后清空eden和这个S0区,此时S0区是空的,然后将S0区和S1区交换,即保持S1区为空,如此往复。因此S0区和S1区中总会有一个区域为空,目的就是为了交换存活对象。
# Major GC/Full GC
当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。这样会有很多对象会被复制很多次(每复制一次,对象的年龄就+1),默认情况下,当对象被复制了15次(这个次数可以通过:-XX:MaxTenuringThreshold来配置),就会进入年老代了。
或者是当S1区不足以存放eden和S0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC(Major GC),也就是新生代、老年代都进行回收。
注意:有些资料说Major GC 是清理老年代,Full GC 是清理整个堆空间—包括年轻代和老年代。但实际上许多 Major GC 是由 Minor GC 触发的,所以很多情况下将这两种GC分离是不太可能的。
# 垃圾收集器
# 垃圾收集器组合
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。不同厂商,不同版本的垃圾收集器有很大的区别,虚拟机规范中并没有明确规定垃圾收集器应该如何实现,本文主要介绍HotSpot虚拟机中的垃圾收集器。
JDK7/8后,HotSpot虚拟机所有收集器及组合(连线),如下图:

图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。
黑色虚线(ParNew和Serial Old)代表原来可以搭配使用,但在JDK8以后已经过时了,不推荐搭配使用。白色虚线(Serial和CMS)则代表原来可以搭配使用,但在JDK8以后已经不支持了。
虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器:
- 新生代收集器:Serial、ParNew、Parallel Scavenge
- 老年代收集器:Serial Old、CMS、Parallel Old
- 整堆收集器:G1
其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案(所以他们之间也有连线),接下来逐一介绍这些收集器的特性、基本原理和使用场景,但要明确一个观点:没有最好的收集器,更没有万能的收集
# 新生代收集器---Serial
Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器,JDK1.3.1前是HotSpot新生代收集的唯一选择。这个收集器是一个单线程的收集器,只会用一个CPU或一条收集线程去完成垃圾回收工作,并且在进行垃圾回收时,必须暂停所有其他的线程(Stop The World),直到收集结束为止。
由于是新生代收集器,所以采用的是复制算法。其运行示意图如下:

Serial收集器看上去似乎简单到有些简陋,但它并不过时,实际上它依然是虚拟机运行在Client模式(桌面)下的默认新生代收集器。它的优点在于:简单而高效(与其他收集器的单线程比)。对于限定按单个CPU的环境来说,Serial收集器由于没有线程交互的开销,因而可以获得最高的单线程收集效率。
在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代,停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生,这点停顿是可以接受的。所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。
# 老年代收集器---Serial Old
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法,收集的过程中需要暂停所有用户线程(Stop The World)。其运行示意图如下:

这个收集器的主要意义也是在于给Client模式下的虚拟机使用,如果在Server模式下,那么它主要还有两大用途:
- 在JDK1.5之前的版本中与Parallel Scavenge收集器搭配使用
- 作为CMS收集器的的后背方案,在并发收集发生Concurrent Mode Failure时使用
# 新生代收集器---ParNew
ParNew收集器其实就是Serial收集器的多线程版本。同样是新生代收集器;除了使用多个线程进行垃圾回收之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都和Serial收集器完全一样。两种收集器在实现上甚至都共用了相当多的代码。
ParNew收集器的工作过程如图所示:

ParNew收集器是许多运行在Server模式下的虚拟机首选的新生代收集器,因为除Serial外,目前只有它能与CMS收集器配合工作,而CMS收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
并行(Parallel):多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上
ParNew收集器在单CPU环境下性能不会比Serial更好,因为存在线程交互的开销。
当然,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU的数最相同,在多CPU的环境下它可以有效的利用资源。
# 新生代收集器---Parallel Scavenge
和ParNew收集器一样,Parallel Scavenge收集器是也并行的多线程新生代垃圾收集器,也是采用复制算法,那它有什么特别之处呢?
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。
吞吐量(Throughput):CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那么吞吐量就是99%。
停顿时间越短则响应速度越快,用户体验越好,所以更适合交互性强的服务程序。而吞吐量高可以高效利用CPU资源,尽快完成程序的运算,主要适合后台运算多交互少的任务,例如那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
# 老年代收集器---Parallel Old
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法;这个收集器是在JDK1.6才开始提供的。其工作过程如图所示:

在JDK1.6之前新生代的Parallel Scavenge收集器一直处于比较尴尬的境地,它只能和Serial Old收集器搭配使用,但是由于Serial Old在服务端的性能较差, 所以根本无法获得吞吐量最大化的效果。
直到Parallel Old收集器出现后,才真正形成了吞吐量优先的组合;在注重吞吐量和CPU资源敏感的场合,都可以优先考虑Parallel Scavenge新生代加Parallel Old老年代收集器的策略。
# 老年代收集器---CMS
CMS(Concurrent Mark Sweep)收集器的目的是把回收停顿时间降低到最短;对于B/S架构的互联网系统,要求服务器的响应速度较高,则适合使用CMS收集器。
从名字(包含Mark Sweep)上可以看出,它是基于标记清除算法实现的。它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤:1、初始标记(CMS initial mark) 2、并发标记(CMS concurrent mark) 3、重新标记 (CMS remark) 4、并发清除(CMS concurrent sweep)。
CMS收集器的工作过程如图所示:

初始标记和重新标记两个过程依然要停止用户线程(Stop The Word)。
- 初始标记:仅仅只是标记一下GCRoot能直接关联到的对象,单线程,速度很快。但需要"Stop The World"
- 并发标记:进行GCRoot Tracing的过程。对初始标记中标记的对象进行全路径的扫描,标记出存活对象。此时用户线程也在运行
- 重新标记:多线程,STW。修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短
- 并发清除:回收所有的垃圾对象。此时用户线程也在运行
整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作,所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;
看起来非常美好,但CMS收集器也有3个明显的缺点:
- 对CPU资源非常敏感:并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。CMS的默认收集线程数量是=(CPU数量+3)/4;当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。
- 无法处理浮动垃圾:在并发清除时,用户线程新产生的垃圾,称为浮动垃圾。浮动垃圾使得并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集。如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败;这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生;这样的代价是很大的。
- 产生大量内存碎片:由于CMS基于"标记-清除"算法,这意味着收集结束时会有大量空间碎片产生。这会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。为了解决这个问题,CMS收集器提供了一个-XX: +UseCMSCompactAtFuHCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。
# G1收集器
# 概述
G1(Garbage First)垃圾收集器是当今垃圾回收技术最前沿的成果之一。2012年才在jdk1.7u4中可以使用,Oracle在jdk1.9中将G1变为默认的垃圾收集器,来代替CMS,大有一统天下的气势。所以这里单独起个大标题分析它,深入了解下。
G1是一款面向服务端应用的垃圾收集器,其设计原则是"首先收集尽可能多的垃圾(Garbage First)"。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,找出具有高收集收益的分区进行收集。与其他GC收集器相比,G1具备如下特点:
- 并发与并行:G1能充分利用多CPU、多核环境的硬件优势,缩短STW(stop the word)停顿时间
- 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。不需要其他收集器配合就能管理整个GC堆,同时管理新生代和老年代
- 空间整理:整体上基于标记-整理算法,局部基于复制算法,这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存
- 可预测的停顿:G1除了降低了STW时间外,还能建立可预测的停顿时间模型,比如指定在M毫秒内,消耗在GC上时间不得超过N毫秒
# 分区(Region)
前面介绍的几种垃圾收集器中年轻代、老年代都是独立且连续的内存块,而G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。
G1同样也使用分代收集策略,将堆分为Eden、Survivior、Old等,只不过是按照逻辑划分的,每个Region逻辑上属于一个分代区域,并且在物理上不连续,当一个Old的Region收集完成后会变成新可用Region并可能成为下一个Eden Region。每个Region不会确定地为某个代服务,可以按需在年轻代和老年代之间切换,再也不用单独设置每个代的大小了,也不用担心它们的内存是否足够。
当申请的对象大于Region大小的一半时,会被放入一个Humongous Region(巨型区域)中。当一个Region中是空的时,称为可用Region或新Region。

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
# Card和RSet
在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),分配的对象会占用物理上连续的若干个Card。
在串行和并行收集器中,GC通过整堆扫描,来确定对象是否处于可达路径中。然而G1为了避免STW式的整堆扫描,在每个分区记录了一个已记忆集合(Remember Set),简称RSet。
逻辑上每一个Region都有一个Rset,Rest使用points-in的方式记录points-out引用(即记录谁引用了我,而不是我引用了谁)。如下图所示:

事实上,并非所有的引用都需要记录在RSet中,如果一个分区确定需要扫描,那么无需RSet也可以无遗漏的得到引用关系。那么引用源自本分区的对象,当然不用落入RSet中;同时,G1 GC每次都会对年轻代进行整体收集,因此引用源自年轻代的对象,也不需要在RSet中记录。所以RSet主要记录young->old、old->old之间的引用。
虽然RSet名字里带个Set,但它其实是HashTable结构。其Key值为引用对象所在Region的起始地址、Value为引用对象所在的Card索引数组。

上面的示意图向我们展示三个region,其中R1、R3为老年代,R2为新生代Eden区,R2的RSets记录了R1中索引为1、3及R3中索引为2、4、6的card对它的引用。
这样在回收年轻代时,只要扫描所有的年轻代Region的Rset,就可以确认所有老年代到年轻代的引用,不用扫描整个老年代。虽然付出了空间、维护的代价,却带来了GC回收效率的提升。这是典型的空间换时间的策略。
# Young GC
Young GC收集年轻代里的Region,主要是对Eden区进行GC,STW且 并行执行,以 RSet作为根集扫描获取存活对象,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空。
同时,年轻代收集还负责维护对象的年龄(存活次数),辅助判断老化(tenuring)对象晋升的时候是到Survivor分区还是到老年代分区。年轻代收集首先先将晋升对象尺寸总和、对象年龄信息维护到年龄表中,再根据年龄表、Survivor尺寸、Survivor填充容量-XX:TargetSurvivorRatio(默认50%)、最大任期阈值-XX:MaxTenuringThreshold(默认15),计算出一个恰当的任期阈值,凡是超过任期阈值的对象都会被晋升到老年代。
# Mixed GC
与Young GC 关注 Survivor区域不同,Mixed GC目标在全堆,且通常伴随执行一次 Young GC。它回收年轻代的所有Region、以及全局并发标记阶段选出的老年代Region。
当old region的对象占总Heap的比例超过阈值(默认45%)之后,就会开始并发标记(Concurent Marking), 完成并发标记 后,G1会从Young GC切换到Mixed GC, 在Mixed GC中,G1可以增加若干个Old区域的Region到CSet(Collection Set,收集集合,记录需要回收对象的集合)中。
-XX:InitiatingHeapOccupancyPercent=45,开始一个标记周期的堆占用比例阈值,默认45%,注意这里是整个堆,不同于CMS中的Old堆比例。
老生代的G1垃圾回收有以下几个关键点:
- 全局并发标记(global concurrent marking): 标记存活的对象、并且计算各个Region的活跃度。活跃度(liveness)信息标记出哪些区域块最适合回收,在转移暂停期间最适合回收掉
- 拷贝存活对象(evacuation)
其工作过程如图所示:

- 初始标记(initial mark,STW):暂停所有线程,标记出所有可以直接从GC roots可以到达的对象,这是在Young GC的暂停收集阶段顺带进行的
- 根区域扫描(root region scan):找出所有的GC Roots的Region, 然后从这些Region开始标记可到达的对象。
- 并发标记(Concurrent Marking):在整个堆中查找存活的对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断
- 最终标记(Remark,STW):清空SATB缓冲区,跟踪未被访问的存活对象,并执行引用处理。
- 清除垃圾(Cleanup,STW):执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC会识别完全空闲的区域和可供进行混合垃圾回收的区域
# Full GC
G1的一些收集过程是和应用程序并发执行的,所以可能还没有回收完成,是由于申请内存的速度比回收速度快,新的对象 就占满了所有空间,在CMS中叫做Concurrent Mode Failure, 在G1中称为Allocation Failure,这时候就会触发Full GC。
然而G1是不提供Full GC的,因此会触发Serial Old,使用单线程进行FullGC,一旦FullGC对性能影响十分严重。
# 推荐使用G1的场景
个人认为更换GC或者进行调优只能算是系统的锦上添花,并不能作为主要解决系统性能问题的关键,出现内存问题时,应当以修改应用代码为主、编写清晰的GC友好的代码,选择与应用场景合适的收集器可以提高系统的性能。
当然,随着Oracle对G1的持续改进,我相信他可以代替CMS。但如果你现在采用的收集器没有出现问题,那就没有任何理由现在去选择G1。G1收集器推荐用于需要大堆(大小约为6 GB或更大)且GC延迟要求有限的应用(稳定且可预测的暂停时间低于0.5秒)。
如果你的应用追求低停顿,那G1现在已经可以作为一个可尝试的选择,如果你的应用追求吞吐量,那G1并不会为你带来什么特别的好处。
# 垃圾回收器组合及参数设置
经过上面的学习,我们发现可用的垃圾回收器组合只剩下4种了:
| 新生代 | 老年代 | JVM参数 |
|---|---|---|
| Serial | Serial Old | -XX:+UseSerialGC |
| ParNew | CMS + Serial Old | -XX:+UseParNewGC -XX:+UseConcMarkSweepGC jdk1.8种可以不使用第二 个参数 |
| Parallel Scavenge | Parallel Old | -XX:+UseParallelGC或者-XX:+UseParallelOldGC |
| G1 | G1 | -XX:+UseG1GC |
- -XX:+UseSerialGC:允许使用串行垃圾收集器。对于不需要垃圾收集的任何特殊功能的小型和简单应用程序,这通常是最佳选择。默认情况下,禁用此选项,并根据计算机的配置和JVM的类型自动选择收集器
- -XX:+UseParNewGC:允许在年轻代中使用并行线程进行收集。默认情况下,禁用此选项。设置-XX:+UseConcMarkSweepGC选项时会自动启用它
- -XX:+UseConcMarkSweepGC:允许为老年代使用CMS垃圾收集器。默认情况下,禁用此选项,并根据计算机的配置和JVM的类型自动选择收集器。启用此选项后,将-XX:+UseParNewGC自动设置该选项
- -XX:+UseParallelGC:允许使用并行清除垃圾收集器(也称为吞吐量收集器),通过利用多个处理器来提高应用程序的性能。默认情况下,禁用此选项,并根据计算机的配置和JVM的类型自动选择收集器。如果已启用,则会-XX:+UseParallelOldGC自动启用该选项,除非你明确禁用它。
- -XX:+UseParallelOldGC:允许将并行垃圾收集器用于完整的GC。默认情况下,禁用此选项。启用它会自动启用该-XX:+UseParallelGC选项
- -XX:+UseG1GC:允许使用垃圾优先(G1)垃圾收集器。默认情况下,禁用此选项,并根据计算机的配置和JVM的类型自动选择收集器
java -XX:+PrintCommandLineFlags -XX:+UseSerialGC -version
java -XX:+PrintCommandLineFlags -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -version
java -XX:+PrintCommandLineFlags -XX:+UseParallelGC -version
java -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC -version
java -XX:+PrintCommandLineFlags -XX:+UseG1GC -version
java -XX:+PrintCommandLineFlags -XX:+UseSerialGC -XX:+UseConcMarkSweepGC –version //非法
java -XX:+PrintCommandLineFlags -XX:+UseParNewGC -XX:-UseConcMarkSweepGC –version //废弃
2
3
4
5
6
7