【开局一段话,再送一张图】
如果想要最小化地使用内存和串型回收,选 Serial GC;
如果想要最大化应用程序的吞吐量,选Parallel GC;
如果想要最小化GC的中断或停顿时间,选 CMS GC;
如果兼顾低延时和吞吐量,选G1
垃圾收集器没有在JVM规范中进行过多的规定,可以由不同的厂商来实现
Oracle:CMS、G1
IBM:J9
ReadHeat:Shenandoah GC
评估GC性能的重要指标:
吞吐量:运行用户代码的时间占总运行时间的比例
暂停时间[STW]:执行GC线程时,用户线程被暂停的时间
内存占用:Java堆区所占的内存大小
吞吐量
吞吐量就是CPU运行用户代码的时间与CPU总消耗时间的比值,即:
吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)
吞吐量高是降低了内存回收的执行频率
比如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞量就是99%
暂停时间
暂停时间是指一个时间段内应用程序线程暂停,让GC线程执行的状态,例如:
GC期间100毫秒的暂停时间,意味着在这100毫秒期间内没有应用程序线程是活动的
暂停时间短,但是频繁的执行内存回收
这两个指标本质上是互斥的,我们只能在最大吞吐量优先的情况下,降低停顿时间。
垃圾回收器与JVM是紧密相连的,不同厂商的JVM有着不同的垃圾回收器,不同的垃圾回收器有不同的GC。具体到什么场景下的使用
用户交互或B/S场景:注重低延迟
服务器:注重高吞吐量
高吞吐量:
高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如:
执行批量处理
订单处理
工资支付
科学计算的应用程序
7款经典收集器与垃圾分代之间的关系:
Hotspot虚拟机在JDK14及之前所有收集器组合:
(红色虚线)由于维护和兼容性测试的成本,在JDK8时将 Serial + CMS、 ParNew + Serial Old这两个组合声明为 Deprecated(EP173),并在JDK9中完全取移除Remove这些组合的支持(JEP214)
(绿色虚线)JDK14中:弃用Parallel Scavenge和 Serial Old GC组合
(JEP366)(青色虚线)JDK14中:删除CMS垃圾回收器(JEP363)
如何查看默认的垃圾收集器:
-XX:+PrintCommandLineflags
:查看命令行相关参数(包含使用的垃圾收集器)
使用命令行指令:jinfo-flag
相关垃圾回收器参数进程ID
为什么会出现G1呢?
官方给G1设定的目标是:
在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起 “全功能收集器” 的重任与期望。
前提:延迟可控(降低STW频率)
提升:提高吞吐量
JDK9及之后版本默认的垃圾回收器
G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。使用不同的 Region来表示Eden、S0区,S1区,Old区等。
每次根据允许的收集时间,优先回收价值最大的Region。
G1(Garbage- First)是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。
在JDK1.7版本正式启用,是JDK9以后的默认垃圾回收器,取代了CMS回收器以及Parallel、Parallel Old组合,被Oracle官方称为 “全功能的垃圾收集器”。
极高概率满足是指:降低STW的阈值,同时兼顾高吞吐量
1. 并行与并发
并行性:G1在回收期间,可以有多个GC线程同时工作(不再是一个GC线程),有效利用多核计算能力,此时用户线程处于STW
并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
并发:用户线程与GC线程同步执行,STW很短(STW频率高),低延时;
并行:提高了吞吐量,多个GC线程同时工作,但是相应的STW会更长(STW频率低)
2. 分区收集
从分区上看,G1依然属于分区型垃圾回收器,同时兼顾年轻代和老年代。将堆空间分为若干个小区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
3. 空间整合
G1将内存划分为一个个的Region,采用标记-整理算法(Mark-Compact)
4. 可预测的停顿时间模型
回收时间可预测性,每次根据允许的时间优先回收价值最大的Region,尽可能提高收集效率
【分区Region】
G1的设计原则就是简化JVM性能调优,发人员只需要简单的三步即可完成调优:
第一步:开启G1垃圾收集器
第二步:设置堆的最大内存
第三步:设置最大的停顿时间STW
G1中提供了三种垃圾回收模式:YoungGC、 Mixed GC和Full GC,在不同的条件下被触发。
【参数列表】:
-XX:+UseG1GC
:手动指定使用G1收集器执行内存回收任务。
-XX:G1HeapRegionSize
:设置每个Region的大小。值是2的雾,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。
-XX:MaxGCPauseMillis
:设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms
-XX:ParallelGCThread
:设置STW工作线程数的值。最多设置为8
-XX:ConcGCThreads
:设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。
-XX:InitiatingHeapoccupancyPercent
:设置触发并发GC周期的Java堆占用率阙值。超过此值,就触发GC。默认值是45。
【局部的GCRoot】一个对象被不同区域引用的问题如何解决?
新生代的对象被老年代对象所引用,YangGC时无法将其回收
一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,
判断对象存活时,是否需要扫描整个Java堆才能保证准确?
在其他的分代收集器,也存在这样的问题(而G1更突出)
回收新生代也不得不同时扫描老年代? 这样的话会降低Minor GC的效率
【解决方法】
无论G1还是其他分代收集器,JVM都是使用记忆集(Remembered Set) 来避免全局扫描
每个Region都有一个对应的Remembered Set;
每次Reference类型数据写操作时,都会产生一个 写屏障(Write Barrier) 暂时中断操作
然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(检查老年代对象是否引用了新生代对象);
如果不同,通过Card Table把相关引用信息记录到引用指向对象的所在Region对应的
Remembered Set中;
当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set就可以保证不进行全局扫描,也不会有遗漏。
G1的垃圾回收过程主要包括如下三个环节:
新生代代GC(Young GC)
老年代并发标记过程(Concurrent Marking)同时伴随Yang GC
混合回收(Mixed GC):新生代和老年代同时回收
如果需要,单线程、独占式、高强度的Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收
在JDK1.5时期,HotSpot推出CMS(Concurrent-Mark-Sweep)收集器,这款收集器HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
CMS的垃圾收集算法采用标记-清除算法,并且也会"Stop-The-World"
这里所指的同时工作是用户线程和GC线程同时并发执行,尽可能减少STW,只能是尽可能地规避STW,无法消除
CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验,CMS收集器就非常符合这类应用的需求。
CMS作为老年代的收集器,却无法与新生代收集器Parallel Scavenge配合工作,所以在JDK1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。
初始标记:标记出GC Roots(STW非常短暂)
并发标记:标记可达对象,GC线程与用户线程并发执行(并发不会产生STW)
重新标记:修正标记并发期间产生的新垃圾(STW非常短暂)
并发清理:并发清除死亡对象,与用户线程并发执行(清除算法不会STW,但是存在碎片化)
由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的
小结:
垃圾回收器目前规避STW是不可能的,只能是与用户线程并发执行缩短STW时间
CMS就是并发有效的缩短了STW
另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阀值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。
如果在重新标记阶段,用户线程产生了非常多的垃圾,超出了垃圾回收线程的回收空间,就会出现Concurrent Mode Failure
,则STW,临时启用Serial Old收集器来重新进行老年代回收。
JDK14移除了CMS
有人会觉得既然 Mark Sweep会造成内存碎片,那么为什么不把算法换成Mark Compac呢?
Mark Compact更适合“ Stop The World”这种场景下使用。
【CMS的优点】
并发收集
低延迟
【CMS的弊端】
会产生内存碎片,导致并发清除后,用户线程可用的空间不足。 在无法分配大对象的情况下,不得不提前触Full GC
CMS收集器对CPU资源非常敏感。 在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低
CMS收集器无法处理浮动垃圾。 可能出现“Concurrent Mode Failure"失败而导致另一次Full GC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。
Serial收集器是最基本、历史最悠久的垃圾收集器了。JDK1.3之前回收新生代唯一的选择。
Serial收集器采用复制算法、串行回收和Stop The World
机制的方式执行内存回收。
除了年轻代之外, Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器。
Serial Old收集器同样也采用了串行回收和Stop The World
机制,只不过内存回收算法使用的是标记-压缩算法
这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束( Stop the World)
【参数设置】
-XX:+UseSerialGC
:表明新生代使用 Serial GC,同时老年代使用 SerialOld GC应用场景:
ParNew是Serial的多线程版本
New:只能处理新生代区域
ParDew收集器采用并行回收的方式执行内存回收外,其余与Serial之间几乎没有任何区别。
ParDew收集器在年轻代中同样也是采用复制算法、"Stop-The-World"机制
【参数设置】
-XX:+UseSerialGC
:表明新生代使用 Serial GC,同时老年代使用 SerialOld GC
-XX:+UseParNewGC
:表明新生代使用 ParDew GC
【小结】
单CPU:使用Serial回收器
多CPU或多核心:使用ParNew,提高吞吐量
Parallel Scavenge收集器同样也采用了复制算法、并行回收和"Stop The World"机制。
和ParDew收集器不同,Paralllel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器
自适应调节策略也是 Parallel Scavenge与 ParNew一个重要区别
自适应模式下,年轻代的大小、Eden和 Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点
【参数配置】
-XX:+UseParallelGC
手行内存回收任务
-XX:+UseParallelOldGC
(JDK8默认开启)
这两个参数互相激活,一个使用则另一个也被使用
ZGC与 Shenandoah目标高度相似,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。
《深入理解Java虚拟机》一书中这样定义ZGC:
ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-压缩算法的,以低延迟为首要目标的一款垃圾收集器
ZGC的工作过程可以分为4个阶段:并发标记、并发预备重分配、并发重分配、并发重映射
【吞吐量测试数据】
【10ms内低延时】
ZGC几乎在所有地方并发执行的,除了初始标记的是STW的。所以停顿时间几乎就耗费在初始标记上,这部分的实际时间是非常少的。所以它能保证将延时控制在10ms
以内
ZGC可能将会取代G1,是未来JDK的首选。