相关概念
并行和并发
并行(Parallel)
指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent)
指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行。而垃圾收集程序运行在另一个CPU上。
Minor GC 和 Full GC
新生代GC(Minor GC)
指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC / Full GC)
指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
吞吐量(Throughput)
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即
吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
假设虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
GC Root
GC Root 包含以下对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
Serial/Serial Old 收集器
这是两款最基本的垃圾回收器,一般用于客户端模式下 ,适合单核或者配置较低的情况。曾经(JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。
Serial
新生代垃圾收集器,标记复制算法 ,单个垃圾收集线程,会产生Stop The World。
bash
# 添加该参数来显式的使用Serial垃圾收集器。
-XX:+UseSerialGC
Serial Old
老年代垃圾收集器,标记-整理算法 ,单个垃圾收集线程,Stop The World,可作为CMS收集器并发失败时的后备预案。
Par New / Parallel Old 收集器
相比于Serial/Serial Old,这两款垃圾收集器也很类似,主要区别是引入了多个垃圾收集线程,但收集的时候仍然是需要Stop The World的,无法与用户线程并发。
Par New
新生代垃圾收集器,标记复制算法 ,多个垃圾收集线程,Stop The World,经常与CMS垃圾收集器搭配使用。
bash
# 指定使用CMS后,会默认使用ParNew作为新生代收集器。
-XX:+UseConcMarkSweepGC
# 强制指定使用ParNew。
-XX:+UseParNewGC
# 指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同。
-XX:ParallelGCThreads
Parallel Old
老年代垃圾收集器,标记整理算法 ,多个垃圾收集线程,Stop The World,与Paralle Scavenge收集器搭配使用。
Parallel Scavenge收集器
新生代垃圾收集器 ,与Parallel Old搭配使用,与Par New 类似,采用多个垃圾收集线程,会产生Stop The World,但是相对于Par New,它有着一些独特的特点,优势。
首先它是一款以吞吐量优先的垃圾收集器 ,可以通过-XX:GGTimeRatio
,来达到设置吞吐量的目的。
另一个特点是它有一个自适应调节策略,可通过-XX:UseAdaptiveSizePolicy
这个开关参数开启自适应调节,开启后它将自动调节新生代大小,Eden与Survivor的比例,晋升老年代对象大小等参数值,从而尽量满足设置的吞吐量参数或者停顿时间参数。
bash
# Parallel Scavenge收集器提供了两个参数来用于精确控制吞吐量,
#一是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis参数,
#二是控制吞吐量大小的 -XX:GCTimeRatio参数;
#参数允许的值是一个大于0的毫秒数,收集器将尽可能的保证内存垃圾回收花费的时间不超过设定的值(但是,并不是越小越好,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的,如果设置的值太小,将会导致频繁GC,这样虽然GC停顿时间下来了,但是吞吐量也下来了)。
-XX:MaxGCPauseMillis
# 参数的值是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,默认值是99,就是允许最大1%(即1/(1+99))的垃圾收集时间。
-XX:GCTimeRatio
#参数是一个开关,如果这个参数打开之后,虚拟机会根据当前系统运行情况收集监控信息,动态调整新生代的比例、老年大大小等细节参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略。
-XX:UseAdaptiveSizePolicy
CMS(Concurrent Mark Sweep)收集器
老年代垃圾收集器,与Par New搭配使用。
它是一款以最小回收停顿时间优先的垃圾收集器 ,也是Hotspot第一款真正采用并发收集算法的垃圾收集器 。采用了标记-清除算法,在标记阶段利用增量更新解决并发标记问题。
其缺点也很明显,会产生大量的内存碎片,且吞吐量也不高。在进行Full GC时会对内存碎片利用整理算法进行整理(会Stop The World)。并发清除阶段可能用户线程会没有足够的内存空间分配对象,导致分配失败,此时会以Serial Old收集器作为分配失败的后备预案(会Stop The World)。
CMS收集器工作的整个流程分为以下4个步骤:
- 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要"Stop The World"。
- 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长。
- 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要"Stop The World"。
- 并发清除(CMS concurrent sweep)
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作。
优点
CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿,因此CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)。
缺点
- 对CPU资源非常敏感 其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。
- CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。
- 无法处理浮动垃圾(Floating Garbage) 可能出现"Concurrent Mode Failure"失败而导致另一次Full GC的产生。
- 由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。
- 这一部分垃圾就被称为"浮动垃圾"。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。
- 标记-清除算法导致的空间碎片 CMS是一款基于"标记-清除"算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。
- 空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象。
JVM参数配置
bash
## 新生代 ParNew + 老年代 CMS + 老年代 Serial Old
# 某些版本的参数是这样的: -XX:+UseConcurrentMarkSweepGC
-XX:+UseConcMarkSweepGC
# 响应时间优先,停顿时间,是一个建议时间,GC会尝试用各种手段达到这个时间,比如减小年轻代,默认 18446744073709551615
-XX:MaxGCPauseMillis
# 吞吐量优先,设置JVM吞吐量要达到的目标值, GC时间占用程序运行时间的百分比的差值,默认是 99
# 也就应用程序线程应该运行至少99%的总执行时间,GC占 1%
-XX:GCTimeRatio=99
# 并行收集器(ParNew , STW, YGC)的线程数,默认CPU所支持的线程数,如果CPU所支持的线程数大于8,则 默认 8 + (logical_processor -8)*(5/8)
-XX:+ParallelGCThreads
# CMS垃圾回收线程数量
-XX:ParallelCMSThreads
# 解决 CMS `Memory Fragmentation` 碎片化, 开启FGC时进行压缩,以及多少次FGC之后进行压缩
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=3
# 解决 CMS `Concurrent mode failure` ,`Promotion Failed`晋升失败
# 使用多少比例的老年代后开始CMS收集,默认是68%(近似值),如果频繁发生SerialOld卡顿,应该调小,(频繁CMS回收)
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
# 开启 CMS 元空间的垃圾回收
-XX:+CMSClassUnloadingEnabled
# -XX:CMSInitiatingPermOccupancyFraction (JDK8已经移除)
G1(Garbage First)收集器
以CMS收集器替代者的身份出现,同样以最小回收停顿时间为目标 ,是JDK9的默认垃圾收集器 (JDK8是Parallel Scanvenge + Parallel Old组合)。
它开创了以局部收集思想和面向Region的内存布局,以Region做为最小回收单元,多个Region组成回收机CSet。同样,这款垃圾收集器中也包含着分代收集思想,不过不同于之间的收集器将堆内存划分为新生代,老年代的布局。
G1将堆空间按固定大小(1M-32M可通过启动参数设定)划分为多个Region,每个Region都可以根据需要作为新生代(Eden,Survivor),老年代(Old),或者说大对象(Humongous,大小超过Region一半,当做老年代对待)。
G1的特点是建立了停顿时间模型:在M毫秒的时间片内 消耗在垃圾收集上的时间不超过N毫,通过启动参数+XX:MaxGCPauseMillis
可指定最大停顿时间。
实现原理:在垃圾收集过程中,记录每个Region回收耗时等信息,计算出平均值,标准偏差,置信度等统计信息,从而预测出由哪些Region组成的回收集才可以在不超过设置的停顿时间约束下获得最高的收益。
G1的收集过程如上图所示。其主保证并发标记正确性的解决方案是原始快照(SATB,区别于CMS的增量更新),在筛选回收阶段更新Region统计数据,根据回收价值排序,选择回收集,回收。
所以从整体上看,G1还是基于标记-整理算法的,只实现了标记阶段的与用户线程并发,回收阶段还是要Stop The World。这种方式导致其没有内存碎片问题。但如果能实现其整理阶段的并发,速度就更快了(其实,整理阶段的并发实现还是比价难得,在后来的Shenandoah和ZGC收集器便实现了整理阶段的并发)。
G1具备如下特点
- 并行与并发 G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短"Stop The World"停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
- 分代收集 与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
- 空间整合 G1从整体来看是基于"标记-整理"算法实现的收集器,从局部(两个Region之间)上来看是基于"复制"算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
- 可预测的停顿 这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
G1收集器运行流程
G1收集器的运作大致可划分为以下几个步骤:
- 初始标记(Initial Marking) 仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。
- 并发标记(Concurrent Marking) 从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
- 最终标记(Final Marking) 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
- 筛选回收(Live Data Counting and Evacuation) 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
JVM参数配置
bash
# JDK 9开始为默认垃圾回收器
-XX:+UseG1GC
# 响应时间优先,建议值,设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal)
# JVM 会尽力去达成这个目标. 所以有时候这个目标并不能达成
# G1会尝试调整Young区的块数来达到这个值
-XX:MaxGCPauseMillis
# 响应时间优先,GC的停顿间隔时间,默认0
-XX:GCPauseIntervalMillis
# 吞吐量优先,设置JVM吞吐量要达到的目标值, GC时间占用程序运行时间的百分比的差值,默认是 99
# 也就应用程序线程应该运行至少99%的总执行时间,GC占 1%
-XX:GCTimeRatio=99
# 并发回收器(STW YGC)的工作线程数量,默认CPU所支持的线程数,如果CPU所支持的线程数大于8,则 默认 8 + (logical_processor -8)*(5/8)
-XX:ParallelGCThreads
# G1 并发标记线程数量
-XX:ConcGCThreads
# 启动并发GC时的堆内存占用百分比. G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例。默认45%
# 当堆存活对象占用堆的45%,就会启动G1 中Mixed GC
-XX:InitiatingHeapOccupancyPercent
# G1 分区大小,建议逐渐增大该值,1 2 4 8 16 32。
# 随着size增加,垃圾的存活时间更长,GC间隔更长,但每次GC的时间也会更长。ZGC做了改进(动态区块大小)
-XX:G1HeapRegionSize
# 新生代最小比例,默认为5%
# -XX:G1NewSizePercent (JDK8u23已经移除 https://www.oracle.com/technical-resources/articles/java/g1gc.html)
# 新生代最大比例,默认为60%
# -XX:G1MaxNewSizePercent (JDK823已经移除 https://www.oracle.com/technical-resources/articles/java/g1gc.html)
# G1 新生代初始大小,默认为5%
-XX:NewSize
# G1 新生代最大大小
-XX:MaxNewSize
Shenandoah收集器
这是一款低延迟垃圾收集器 ,其目标是在尽可能对吞吐量影响不大的前提下,实现在任何堆内存大小下都可以把垃圾收集时间限制在10ms以内。它是OpenJDK12正式特性之一,和G1收集器有很多相似之处,甚至公用了一部分源代码。
相比之下,它有许多的改进点。比如实现了并发的整理算法,用连接矩阵替代了G1的记忆集。要实现并发整理却有一个非常大的难点:与用户线程并发的情况下移动对象,用户线程可能在收集线程移动过程中对被移动的对象进行读写访问。Shenandoah采取的解决方案是读屏障+转发指针
读屏障:可以理解为虚拟机层面对对象访问操作的AOP切面,说白了就是在访问对象之前要干点什么事;转发指针:在原有对象布局结构的最前面统一加一个引用字段,在不处于并发移动的情况下,该引用指向对象自己。当对象有了一份新的副本时,只需修改该引用值指向新的副本,虚拟机对旧对向的访问就会转发到新对象上去。
ZGC收集器
它也是一款低延迟收集器 ,目标同Shenandoah一样,从目前的表现来看,比Shenandoah更强大,是JDK11发布的一款垃圾收集器。它的特点是Region具有动态性---动态创建和销毁,动态的区域容量大小。此外它同样实现了并发的整理算法。其实现方案是:染色指针+多重映射
染色指针 :将少量额外的信息直接存储在指针上,在64位操作系统中,Linux系统只支持46位的物理地址空间(64TB),染色指针将这46位中的4位拿出来存储4个标志信息:如对象的三色标记状态、是否进入重分配集(被移动过)等信息;
多重映射:由于处理器可不认识地址中哪几位是真是的寻址地址,哪几位是标记位,所以Hotspot就通过多重映射技术对地址做一个映射,将寻址地址相同,标记位不同的引用都映射到同一块物理地址空间上。
ZGC收集器仅从引用上就可以判断一个对象是否处于重分配集中(回收集),若用户访问了重分配集中的对象,将被内存屏障截获,然后根据Region上的转发表将请求转发到新复制的对象上,同时修正引用值,使其指向新对象。
总结
收集器 | 串行、并行或者并发 | 新生代/老年代 | 算法 | 目标 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下Client模式 |
Serial Old | 串行 | 老生代 | 标记-整理 | 响应速度优先 | 单CPU环境下Client模式、CMS的后备预案 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时在Server模式与CMS配合 |
Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Parallel Old | 并行 | 老生代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并发 | 老生代 | 标记-清除 | 响应速度优先 | 集中在互联网网站或B/S系统服务端的Java应用 |
G1 | 并发 | 新生代和老年代 | 标记-整理+复制算法 | 响应速度优先 | 面向服务端应用,将来替换CMS |
拓展
GC相关的JVM参数
内存溢出
bash
# 生产环境一般再额外增加GC日志参数,OOME HeapDump 参数
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dir/
#日志文件的输出路径
-Xloggc:/path/to/log/dir/gc-%t.log
-XX:+PrintGCCause
#输出GC日志
-XX:+PrintGC
#输出GC的详细日志
-XX:+PrintGCDetails
#输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCTimeStamps
#输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintGCDateStamps
#在进行GC的前后打印出堆的信息
-XX:+PrintHeapAtGC