0. 前言
JVM笔记系列,以JDK1.7为基准,主要以《深入理解Java虚拟机》(第二版)和《Java虚拟机规范(Java SE 7版)》 为参考,主要包括下图所示的五部分内容:1.类加载,2.内存区域,3.垃圾回收,4.JVM参数,5.JVM监控工具。
本人是Java程序员,重点关注这些有助于优化开发、性能调优、问题解决等这些和具体生产密切相关的部分;关于Class的文件结构、编译、指令等部分,可以阅读上述书籍或其它材料。
本文主要记录JVM垃圾回收的相关知识,本文的主要知识点如下:
1. 概述
垃圾回收(Garbage Collection)通常被称简称为GC,是自动释放内存空间的机制。Java 内存区域中的程序计数器、Java栈和Native栈等内存空间都随着线程的死亡而自动清理,因此GC主要发生在堆(Heap)和方法区(Method Area)中。
GC机制决定了哪些内存需要回收,什么时候回收,怎么回收;学习GC机制,有利于我们排查内存溢出、内存泄漏、性能调优。
2. 对象存活判断
判断一个对象是否应该被垃圾回收,要看这个对象是否还存在引用;判断的方法有两种,一种是引用计数,一种是可达性分析。在jdk1.2之前,使用的是引用计数器方法;之后使用是可达性分析的方法。
2.1 引用
GC机制都是和引用(Reference)相关的,在Java中存在四种引用,它们的生命周期各不相同。
1. 强引用:如果强引用存在,JVM宁肯抛出OutOfMemory,也不会回收这种对象。只有引用被释放后,才会被回收。
// 强引用示例 StrongReference
Object obj = new Object();
String str ="hello";
2. 软引用:当内存不足时,才会被垃圾回收器回收,一般用于实现内存敏感(例如图片缓存)的缓存实现。
// 软引用示例 SoftReference
Object obj = new Object();
// sr是对obj的软引用。
SoftReference<Object> sr = new SoftReference<>(obj);
obj = null;
sf.get(); // 当内存不足时,gc后,可能是null
3. 弱引用:如果一个对象仅被WeakReference引用,那么gc时该对象会被回收,无论内存是否充足。
WeakReference<Integer> ref = new WeakReference<>(new Integer(512));
System.out.println(ref.get()); // 512
System.gc();
System.out.println(ref.get()); //gc后 null
4. 虚引用:也称为幽灵引用,虚引用十分脆弱,永远也不能通过get()方法获取到被虚引用的对象。它唯一的作用就是当其指向的对象被回收后,自己加入ReferenceQueue,用作记录该引用指向的对象已被销毁。
ReferenceQueue<String> queue = new ReferenceQueue<>();
String str = "hello";
PhantomReference<String> ref = new PhantomReference<>(str, queue);
System.out.println(ref.get()); // 永远是null
2.2 引用计数
每个对象有一个引用计数的属性,新增一个引用计数器加1,引用释放时减1,计数为0时可以回收。该方法简单,但无法解决对象之间循环引用的问题。
2.3 可达性分析
可达性分析,把所有的引用关系看成一张图,利用根搜索算法,从一个GC Root节点开始搜索,搜索走过的路径成为引用链。当一个对象和所有的GC Root之间都没有路径可以到达的时候,证明此对象是不可达对象,即是无用的对象,可以回收。
在Java中,可以作为GC Root的对象有:
- Java栈中引用的对象
- Native栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
3. 垃圾收集算法
3.1 标记-清除算法
“标记-清除”算法(Mark-Sweep)分为标记和清除两个阶段,标记阶段标记所有需要回收的对象,清除阶段回收所有被标记的对象,如下图所示。
它的主要缺点有两个:一是效率问题,标记和清除的过程效率都不高;二是空间问题,标记清除后会产生大量不连续的内存碎片,在之后的运行过程中,需要分配为大对象分配空间时,可能会找不到连续的内存,从而不得不触发另外的gc动作。
标记清除算法是最基础的垃圾回收算法,后续的算法,都是对本算法的改进。
3.2 复制算法
复制算法(Copying)将可用内存按容量划分为大小相等的两块,每次只使用其中的一块;当一块的内存用完了,就将还或者的对象复制到另一块上面,再把已用过的内存空间一次清理掉,如下图所示。
复制算法实现简单,效率很高,也不用考虑内存碎片的问题,只要移动指针,按序分配内存即可;但是如图所示,内存可用空间缩水一半,空间代价很高。
HotSpot虚拟机中,把Java堆年轻代分为1个Eden + 2个Survivor,默认情况下Eden的大小是Survivor的8倍;当垃圾回收时,将Eden和Survivor中存活的对象复制到另一个Survivor中;也即年轻代可用空间最大为90%。当Survivor空间不够时,还需要老年代进行分配担保。
年轻代适合使用复制算法,因为年轻代上的对象生命周期很短,需要复制的对象较少;老年代是不适合使用复制算法的,因为老年代存活的对象太多,执行的复制操作太多,效率低下,更会浪费巨大的内存空间。
3.3 标记-整理算法
标记-整理算法分为标记和压缩两个过程,标记阶段和“标记-清除”算法一样;整理阶段是把所有存活的对象都向一端移动,之后清理到端边界以外的内存。
比起复制算法,标记-整理算法更适合应用在老年代;比起标记-清除算法,避免了内存碎片问题。
3.4 分代收集算法
分代收集算法,把Java堆分为年轻代和老年代,这样就可以根据各个年代的特点采用最适当的算法。在年轻代中,每次gc都发现大量对象死去,少量对象存活,因此选用复制算法,只需要付出少量存活对象的复制成本。老年代对象存活率高,没有额外空间对其分配担保,因此需要使用标记-整理或者标记-清理算法。
4. 垃圾回收器
垃圾回收器就是垃圾回收算法的具体实现,在HotSpot虚拟机的实现里,共有如下图所示的垃圾回收器。其中适用于年轻代的有Serial、ParNew、Parallel Scavenge,适用于老年代的有Serial Old、CMS、Parallel Old。此外,还有还有G1垃圾回收器同时工作在年轻代和老年代。如果两个垃圾回收器之间存在连线,则说明它们可以搭配使用。
不同的垃圾回收器,有不同的使用场景,没有万能的、最优的垃圾回收器;甚至已经有人提出JEP草案,创建一个无操作的垃圾回收器,一种实际不进行垃圾回收的的GC方式。
汇总表
年轻代 | 老年代 | 启用参数 | 特点说明 | |
---|---|---|---|---|
1 | Serial | Serial Old | -XX:+UseSerialGC | 串行 + 串行 |
2 | Serial | CMS | -XX:+UseConcMarkSweepGC | 串行 + 并发 |
3 | ParNew | Serial Old | -XX:+UseParNewGC | 并行 + 串行 |
4 | ParNew | CMS | -XX:+UseConcMarkSweepGC | 并行 + 并发 |
5 | Parallel Scavenge | Serial Old | -XX:+UseParallelGC | 并行 + 串行 |
6 | Parallel Scavenge | Parallel Old | -XX:+UseParallelOldGC | 并行 + 并行 |
7 | G1 | G1 | -XX:+UseSerialGC | 并发 |
4.1 Serial & Serial Old收集器
Serial(串行)收集器是最古老的垃圾收集器,稳定高效,只使用一个线程做垃圾回收,可能会产生长时间的停顿。Serial收集器是Java Client 模式下的默认收集器。
Serial收集器应用于年轻代,采用复制算法;Serial Old收集器应用于老年代,采用标记-整理算法。下图展现了Serial & Serial Old收集器的运行过程。
JVM控制参数
# Serial + Serial Old,年轻代复制算法,串行;老年代标记-整理,串行。
-XX:+UseSerialGC
4.2 ParNew收集器
ParNew收集器是Serial收集器的多线程版本,应用于年轻代,采用复制算法。ParNew收集器默认的老年代收集器是Serial Old,同时也是老年代收集器CMS的默认搭档。
JVM控制参数
# ParNew + Serial Old,年轻代复制算法,并行;老年代标记-整理,串行。
-XX:+UseParNewGC
# 限制gc线程数量
-XX:ParallelGCThreads
4.3 Parallel Scavenge收集器
和ParNew一样,Parallel Scavenge收集器应用于年轻代,采用复制算法,并行多线程回收;不同地是,Parallel Scavenge收集器更关心吞吐量,而不是stop the world的时间。
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
Parallel Scavenge是Java Server模式下的默认收集器。
JVM控制参数
# Parallel + Serial Old,年轻代复制算法,并行;老年代标记-整理,串行。
-XX:+UseParallelGC
4.4 Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供。Parallel Old 比 Serial Old更适合应用在多CPU的硬件环境里。
JVM控制参数
# Parallel + Parallel Old,年轻代复制算法,并行;老年代标记-整理,并行。
-XX:+UseParallelOldGC
4.5 CMS收集器
CMS(Concurrent Mark Sweep)收集器作用于老年代,是以获取最短停顿时间为目标的收集器,大量应用在互联网或B/S系统的服务端。这类应用重视服务的响应速度,希望系统停顿时间最短,从而给客户带来更快的体验。
顾名思义,使用标记-清除算法,它的运作过程分为以下4步:
1. 初始标记(CMS init mark):仅标记GC Roots能够直接关联到的对象,速度很快;会触发stop the world。
2. 并发标记(CMS concurrent mark):是GC Roots Tracing的过程,即可达性分析的过程。
3. 重新标记(CMS remark):修正并发标记期间因为用户线程继续运作而导致标记产生变动的对象的标记记录;会触发 stop the world。
4. 并发清除(CMS concurrent sweep):是回收对象的过程。并发清除阶段,仍然可能有新的垃圾产生,此时标记过程已经完成,只能在下次gc时才能回收,这部分垃圾被成为“浮动垃圾”。
CMS收集器运行示意如下,在GC过程中,并发标记和并发清除耗时最长;但gc线程能够和用户线程并发工作,所以总体来说CMS收集器的内存回收过程和用户线程是并发地执行,从而达到了并发收集、低停顿的目标。
缺点:
CMS使用标记-清除算法,容易产生内存碎片;CMS提供了XX:+UseCMSCompactAtFullCollection参数(默认开启),用于解决该问题,即在full gc时整理内存,但这会stop the world,导致停顿时间变长。
CMS是CPU敏感的,默认启动的回收线程数是(CPU数量+3)/4,如果CPU核数过少,将会降低吞吐率。
CMS在gc阶段用户线程还需要运行,就需要预留有足够的内存空间给用户线程使用;同时浮动垃圾也会占用内存,因此CMS不能像其它收集器那样,在老年代几乎完全被填满时才执行gc。如果预留的内存不足,则会出现“Concurrent Mode Failure”,此时JVM会临时启动Serial Old来做老年代的垃圾回收。
JDK1.5中,老年代使用68%后,就会被激活;JDK1.6中,这个阀值被调整到92%,我们可以使用数-XX:CMSInitiatingOccupancyFraction参数调整这个阀值。
java8里,使用CMS收集器,触发老年代gc除了超过阀值外,还有一种可能,那就是年轻代gc后无法释放空间,需要老年代来担保分配,但老年代也没有足够的空间来担保,就会导致老年代gc。
JVM控制参数
# CMS + ParNew,年轻代复制算法,并行;老年代标记-清除,并发。
-XX:+UseConcMarkSweepGC
# 默认开启,Full GC后执行碎片整理,引起停顿。
-XX:+ UseCMSCompactAtFullCollection
# 设置进行几次Full GC后,进行一次碎片整理
-XX:+CMSFullGCsBeforeCompaction
# 设定CMS gc线程数
XX:ParallelCMSThreads
# 老年代GC阀值,设太高容易导致Concurrent Mode Failure频发。
-XX:CMSInitiatingOccupancyFraction
4.6 G1收集器
G1收集器设计的目标是用来替换CMS收集器的,与CMS相比,G1基于“标记-整理”算法不会产生内存碎片;G1能充分利用多CPU、多核环境下的硬件优势,追求降低停顿时间,还能建立可预测的停顿时间模型,能让使用户明确指定M毫秒的时间段内,gc的时间不超过N毫秒。
G1收集器内存分区
G1收集器把Java堆若干的Region,每个Region大小为2的倍数,范围在1MB-32MB之间,可能为1,2,4,8,16,32MB。所有的Region有一样的大小,JVM生命周期内不会改变。G1最多有2048个Region,因此推算支持的最大堆为64GB。
如下图所示,年轻代(Eden、Survivor)和老生代(Old)在内存中不再有明确的物理界限,它们分别对应若干个Region;此外,G1中还存在名叫Humongous的特殊区域,用于存放短暂存活的巨型对象。
如果一个对象占用的空间大于Region的一半,G1就会认为这是一个巨型对象;巨型对象默认分配到老年代,但如果其只是短期存活的对象,那就会对垃圾回收产生负面影响;因此G1专门划分了Humongous区域,专门用于短暂存活的巨型对象;如果一个Humongous Region存不下巨型对象,G1会寻找连续的Humongous Region,如果找不到,将会触发Full GC。
Remembered Set
Remembered Set用来记录老年代对象和年轻代对象的引用。
当年轻代发生GC时,需要考虑一个问题,即如何找到所有的根对象?为了避免低效率的全堆扫描,G1引入了RSet(Remembered Set)概念,使用了所谓的point-in方案。Point-in的意思就是记录了哪些分区引用了当前分区的对象,仅把这些对象当作根来扫描,就避免了无效的浪费;由于每次GC时,所有的年轻代都会被扫描,因此只需记录老年代到年轻代之间的引用即可。
一个Region在逻辑上被划分为固定大小的区域,每个区域被成为Card(卡);Card Table 是一个数组,每个index都代表了一个Card的内存地址。当一个Card中的对象被引用时,这个Card的内存地址(index)在Card Table中的值被标记成“0”;Rset会记录这个index记录下来。 Rset其实是个Hash Table,key是Region的起始地址,Value是一个集合,里面的元素是Card Table的index。
如下图所示,“old对象x”持有“对象5”的一个引用,因此“对象5”对应的Card在Card Table中被标记“0”,这些信息又记录在Remembered Set中。
年轻代GC Young GC
G1 Young GC 和ParNew类似,采用复制算法,并行;GC过程是产生Stop-The-World的。
经过Young GC,年轻代(所有的Eden & Survivor Region)中存活的对象被复制到一个或多个空闲的Region中,这些Region就是新的Survivor Region;如果年轻代中的对象年龄达到阈值,将会被复制到Old Region中去;原来那些Region变成空闲Region。
从上述表述可知,判断对象存活是最重要的。判断引用是否存在,除了GCRoot之外,还要根据Remembered Set检测老年代对象对年轻代对象的引用。
G1老年代GC
G1老年代GC和CMS类似,也会产生短暂的停顿(Stop The World),但G1不会产生内存碎片;CMS是“标记-清除”,而G1最后是把存活的对象复制到空闲的Region中,不会产生内存碎片。
G1 老年代GC过程如下:
初始标记 (initial mark, STW):此阶段,对GC Roots进行标记,会产生stop the world,并且会触发一次Young GC。
根区域扫描 (root region scan):在初始标记的存活区中,根据Rset扫描老年代的引用,并标记被应用的对象;该操作与用户线程同时运行,不产生stop the world,并在Young GC之前完成。
并发标记(concurrently marking):G1 GC 在整个堆中查找可访问的(存活的)对象,该操作和用户线程同时运行,不产生stop the world;但有可能被新的一次Young GC中断。
重新标记 (remarking):G1中采用了比CMS更快的初始快照算法(snapshot-at-the-beginning,简称SATB)完成堆内存中存活对象的标记,会产生stop the world。
清除垃圾 (copy、clean up):多线程清除死亡对象,会产生stop the world。G1把回收区域中存活的对象复制到空闲的Region中,把之前Region清空并变为空闲Region。
JVM常用的控制参数
# 开启G1收集器
-XX:+UseG1GC
# 设置最大GC停顿时间(ms),JVM会尽量达到这个目标。
-XX:MaxGCPauseMillis=200
# 启动并发GC时堆内存占用百分比
-XX:InitiatingHeapOccupancyPercent=45
# Region的大小,1-32MB之间。
-XX:G1HeapRegionSize=16M
# 晋升到老年代的年龄阀值
-XX:MaxTenuringThreshold=15
# 设置 STW 工作线程数的值。将n的值设置为逻辑处理器的数量,最多为8
-XX:ParallelGCThreads=n
# 并行标记的线程数。将nParallelGCThreads的1/4左右
-XX:ConcGCThreads=n
# 设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险
-XX:G1ReservePercent=10
# 最大堆的大小,只设置最大堆即可。
-Xmx32g
# 年轻代与老生代的大小比例,默认值为2,不建议设置。
-XX:NewRatio=2
# eden/survivor 空间大小的比例,默认值为8,不建议设置。
-XX:SurvivorRatio=8
5.GC日志格式
我们使用如下的JVM参数打开gc日志,同时我们使用CMS收集器,如前文所提,默认的年轻代收集器是ParNew。
# 使用CMS垃圾收集器
-XX:+UseConcMarkSweepGC
# 打印gc日志详情
-XX:+PrintGCDetails
# 打印时间戳,日历形式
-XX:+PrintGCDateStamps
# 打印时间戳,相对时间(JVM启动后多少秒)
-XX:+PrintGCTimeStamps
# 日志文件的输出路径
-Xloggc:../logs/gc.log
5.1 Young GC
下面是一条Young GC的日志,为了显示方便,手动把它折成了4行。
2017-09-28T16:18:03.722+0800: 128.601: [GC (Allocation Failure)
2017-09-28T16:18:03.722+0800: 128.601: [ParNew: 569996K->32501K(613440K), 0.0191920 secs]
586959K->49464K(2029056K), 0.0194319 secs]
[Times: user=0.10 sys=0.00, real=0.02 secs]
解释如下:
# gc发生的时间。
2017-09-28T16:18:03.722+0800
# jvm启动后的相对时间,单位是秒。
128.601
# 引起GC的原因,本例是因为空间不足分配失败。
GC (Allocation Failure
# 使用的是ParNew收集器,即Young gc
# Young回收前使用空间,回收后使用空间,括号内是Young总大小,gc耗时
[ParNew: 569996K->32501K(613440K), 0.0191920 secs]
# java堆回收前使用空间,回收后使用空间,括号内是堆当前总大小,gc耗时
586959K->49464K(2029056K), 0.0194319 secs]
# user 是gc消耗的所有cpu时间,
# sys 是操作系统调用和等待时间,
# real 是应用程序暂停的时间。
[Times: user=0.10 sys=0.00, real=0.02 secs]
经过本次Minor GC,我们可以观察到:
老年代的大小为1415616K,老年代 = 堆 - 年轻代,即2029056K-613440K。
此次gc没有对象晋升到老年代,年轻代回收了537495K(569996K-33879K)内存,整个堆回收了537495K
(586959K-49464K),年轻代和整个堆回收的空间大小一致,可见没有对象晋升到老年代;如果有对象晋升到老年代,那么堆回收的空间会小于年轻代回收的空间。
通过gc日志可以感受到,java对象生命的脆弱,朝生夕死,譬如人生,循环往复。
5.2 CMS GC
下面是CMS gc的一次过程。这是从服务器上随机拿到一条gc日志, 老年代的空间只使用了16.7%,远低于92%的阀值,居然触发old gc了;查看前面的young gc发现也不是“空间分配担保(Promotion Guarantee)”,也许是方法区引起的,这个有待确认,但不影响我们理解gc日志的格式。
2017-09-28T15:24:56.936+0800: 7.314: [GC (CMS Initial Mark) [1 CMS-initial-mark: 23562K(1415616K)] 556804K(2029056K), 0.0310696 secs] [Times: user=0.21 sys=0.01, real=0.04 secs]
解释:CMS开始执行老年代GC,这是CMS的初始标记阶段,标记GC Roots能够直接关联到的对象,产生stop the world。
23562K:当前老年代使用的空间
1415616K:当前老年代可用容量
556804K:当前堆使用的空间
2029056K:当前堆的容量
2017-09-28T15:24:56.977+0800: 7.355: [CMS-concurrent-mark: 0.010/0.010 secs] [Times: user=0.04 sys=0.00, real=0.01 secs]
解释:并发标记阶段,和用户线程并发执行,不产生stop the world,全是时间标记,不再赘述。
2017-09-28T15:24:56.983+0800: 7.361: [CMS-concurrent-preclean: 0.006/0.006 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
解释:并发预清理阶段,查找在执行并发标记阶段新进入老年代的对象,减少“CMS Final Remark”阶段的工作,可以减少stop the world的时间。
2017-09-28T15:24:56.983+0800: 7.361: [CMS-concurrent-abortable-preclean-start]
2017-09-28T15:24:57.387+0800: 7.765: [GC (Allocation Failure) 2017-09-28T15:24:57.387+0800: 7.765: [ParNew: 611391K->25772K(613440K), 0.0224361 secs] 634953K->53551K(2029056K), 0.0226345 secs] [Times: user=0.08 sys=0.00, real=0.02 secs]
2017-09-28T15:24:59.285+0800: 9.663: [CMS-concurrent-abortable-preclean: 1.916/2.301 secs] [Times: user=8.40 sys=0.35, real=2.30 secs]
解释:cms gc被一次新的minor gc中断了,之后重又执行了preclean。
2017-09-28T15:24:59.285+0800: 9.663: [GC (CMS Final Remark) [YG occupancy: 351441 K (613440 K)]2017-09-28T15:24:59.285+0800: 9.663: [Rescan (parallel) , 0.0257186 secs]2017-09-28T15:24:59.311+0800: 9.689: [weak refs processing, 0.0003551 secs]2017-09-28T15:24:59.311+0800: 9.689: [class unloading, 0.0088418 secs]
2017-09-28T15:24:59.320+0800: 9.698: [scrub symbol table, 0.0098930 secs]2017-09-28T15:24:59.330+0800: 9.708: [scrub string table, 0.0011080 secs][1 CMS-remark: 27778K(1415616K)] 379220K(2029056K), 0.0477818 secs] [Times: user=0.27 sys=0.00, real=0.05 secs]
解释:重新标记阶段,标记老年代全部的存活对象,产生stop the world。
# 年轻代当前使用大小和容量
YG occupancy: 351441 K (613440 K)
# STW,重新扫描老年代存活对象
Rescan (parallel) , 0.0257186 secs
# 子阶段1:弱引用处理
weak refs processing, 0.0003551 secs
# 子阶段2:卸载无用的类
class unloading, 0.0088418 secs
# 子阶段3:清理符号表
scrub symbol table, 0.0098930 secs
# 子阶段3:清理字符串表
scrub string table, 0.0011080 secs
# 在上述阶段之后,老年代使用空间和容量
remark: 27778K(1415616K)
# 整个堆的使用空间和容量
379220K(2029056K)
2017-09-28T15:24:59.333+0800: 9.711: [CMS-concurrent-sweep-start]
2017-09-28T15:24:59.345+0800: 9.722: [CMS-concurrent-sweep: 0.011/0.011 secs] [Times: user=0.05 sys=0.00, real=0.01 secs]
解释:并发清除阶段,移除不用的对象,回收空间。并发执行,不产生STW。
2017-09-28T15:24:59.345+0800: 9.722: [CMS-concurrent-reset-start]
2017-09-28T15:24:59.360+0800: 9.738: [CMS-concurrent-reset: 0.015/0.015 secs] [Times: user=0.05 sys=0.01, real=0.02 secs]
解释:重新设置CMS算法内部的数据结构,准备下一个CMS生命周期的使用。
5.3 CMS的两个见的full gc
1.Prommotion failed
新生代采用复制算法进行垃圾收集,所以有可能会由于“空间分配担保(Promotion Guarantee)”导致这次minor gc扩展为一次full gc。
新生代默认被分为eden:s0:s1 = 8:1:1,单个survivor其实只有young区总空间的10%。如果young区minor gc发现所有的object都是活的,survivor无法承受,只好由old区来担保。如果old区没有足够的空间来担保,minor gc就升级为full gc了。
2.concurrent mode failure
concurrent mode failure产生的原理:CMS并发处理阶段,用户线程还在运行中,伴随着程序运行会有新的垃圾产生,CMS无法处理掉它们(没有标记),只能在下一次GC的时候处理。如果预留的空间不够无法满足用户线程的需求,就会出现"Concurrent Mode Failure"失败。这时,虚拟机将会启动备案操作:临时启动Serial Old 收集器来重新进行老年代的垃圾收集,Serial Old收集器会Stop the world,这样会导致停顿时间过长。
虽然可以通过CMSInitiatingOccupancyFraction参数来提搞预留的空间,但过低的CMSInitiatingOccupancyFraction会导致频繁的cms,从而降低性能。
(完)