一、GC分类与性能指标
- 不同厂商、不同版本的JVM可有自己的实现。
- 不同角度有不同分类。
Java不同版本的新特性
- 语法层面:
Lambda,switch,自动装箱、拆箱、enum、泛型。 - API层面:Stream API,新的日期时间,Optional,String,集合框架
- 底层优化:
JVM的GC变化、元空间、静态域、字符串常量池等。
1. 分类
1) 按线程数分
- 串行垃圾回收器
- 适用于单核或较小内存等资源受限的场景。
- 并行垃圾回收器
- 适用于并发能力较强的CPU。
- 并行使得多个CPU同时执行,提升了应用的吞吐量。
- 仍然采用独占式的STW机制。
2)按工作模式分
- 并发式
- GC线程与用户线程交替工作;
- 尽可能减少用户线程停顿时间。
- 独占式
- GC时候停止用户线程(STW)。
3)按碎片处理方式
- 压缩式:执行碎片空间处理。
- 非压缩式:不执行碎片空间处理。
4) 按工作内存区间
- 年轻代垃圾回收器
- 老年代垃圾回收器
2. 性能指标
吞吐量、暂停时间、内存占用三者共同构成【不可能三角】,尽可能满足其中两项。
目前的标准:在最大吞吐量优先的情况下,降低停顿时间。
1) 吞吐量(Throughput)
用户代码运行时间占比总运行时间。
2) 垃圾收集开销
吞吐量的补数,GC时间占比总运行时间。
3) 暂停时间
4) 收集频率
相对于应用程序执行,收集操作的发生频率。
5) 内存占用
Java堆区所占内存的大小。
6) 快速
一个对象从诞生到被回收经历的时间。
二、不同垃圾回收器概述
1. 发展历史
- 1999年随JDK1.3.1一起来的是串行方式的serial GC ,它是第一款GC。ParNew垃圾收集器是serial收集器的多线程版本
- 2002年2月26日,JDK1.4.2,Parallel GC 和Concurrent Mark Sweep GC一起发布
- 在JDK6之后,Para1lel Gc成为HotSpot默认GC.
- 2012年,在JDK1.7u4版本中,G1可用。
- 2017年,JDK9中G1变成默认的GC,以替代CMS。
- 2018年3月,JDK10中G1 GC的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟。
- 2018年9月,JDK11发布。引入Epsilon(ε)垃圾回收器,又被称为【No-op】(无操作)回收器。同时,引入ZGC:可伸缩的低延迟垃圾回收器(Experimental)。
- 2019年3月,JDK12发布。增强G1 GC,自动返回未用堆内存给操作系统。同时,引入Shenandoah GC:低停顿时间的GC (Experimental)。
- 2019年9月,JDK13发布。增强ZGC,自动返回未用堆内存给操作系统。
- 2020年3月,JDK14发布。删除CMS垃圾回收器。扩展ZGC在macos和windows
上的应用
2. 垃圾收集器之间的配合关系
- 没有最优秀的垃圾回收器,只能根据使用场景选出最优的组合。
查看默认垃圾回收器
- VM参数
-XX:+PrintCommandLineFlags (查看命令行相关参数,包含使用的垃圾回收器) - 使用命令行指令
jinfo -flag [相关垃圾回收器参数] [进程ID]
三、Serial回收器:串行回收
1. 年轻代——Serial
- JDK 1.3之前的唯一选择。
- Hotspot VM在Client模式下,新生代默认。
- 采用复制算法、串行回收、STW机制。
2. 老年代——Serial Old
- Client模式下,老年代默认。
- 采用标记-压缩算法、串行回收、STW机制。
- Server模式下,两个用途:
- 与新生代的Parallel Scavenge配合使用。
- 作为CMS的后备方案。
3. 优势
简单而高效。对于限定单CPU环境来说,无线程间交互的开销。
4. 使用
VM参数:-XX:+UseSerialGC
四、ParNew回收器:并行回收
1. 概述
- 是Parallel New缩写,用于处理新生代的垃圾回收,是Serial的多线程版本(新生代回收频繁,并行方式更高效)。
- 采用复制算法、STW机制。
- 很多JVM在Server模式下新生代默认。
- 适用于多核CPU环境,充分利用硬件资源提升吞吐量。在单核CPU环境,性能不及Serial。
2. 适用
- VM参数设置启用:
- -XX:+UseParNewGC
- 限制线程数量:
- -XX:ParallelGCThreads,默认等于CPU线程数
五、Parallel 收集器:吞吐量优先
Java 8中默认组合。
1. 年轻代——Parallel Scavenge
- 采用复制算法、并行回收、STW机制。
- 可控制的吞吐量。
- 适合后台运算、不需要太多交互的场景。
- 自适应调节策略。
2. 老年代——Parallel Old
- JDK 1.6时提出,用于替换Serial Old。
- 采用标记-压缩算法、并行回收、STW机制。
3. 参数设置
- -XX:+UseParallelGC:手动指定年轻代使用Parallel并行收集器。
- -XX:+UseParallelOldGC:手动指定老年代使用Parallel并行收集器。
以上两个参数是一组,设置一个后,会互相激活。
- -XX:ParallelGCThreads:设置年轻代并行收集器线程数。最好与CPU核心数相等,过多线程影响性能。
- CPU核心数 ≤ 8——等于cpu_count
- CPU核心数>8—— 3 + (5 * cpu_count) / 8
- -XX:MaxGCPauseMillis:设置STW最大停顿时间,单位ms。需谨慎使用。
- 为了尽可能满足(可能会超过)该限制,JVM工作时会适当调整堆大小或其他参数。
- 对用户来讲,停顿时间越短越好。服务端注重高并发及吞吐量,所以更适合Parallel进行控制。
- -XX:GCTimeRatio:设置垃圾收集时间占比总时间。用于衡量吞吐量的大小。
- 取值范围(0, 100),默认99。
- 与上一个参数-XX:MaxGCPauseMillis有一定的矛盾性。
- -XX:+UseAdaptiveSizePolicy:设置Parallel Scavenge的自适应调节策略。
- 这种模式下,会自动调整年轻代大小、Eden和Survivor比例、老年代晋升年龄等。来确保堆大小、吞吐量、停顿时间之间的平衡点。
- 适用于手动调优困难的场合。仅需指定最大堆内存、吞吐量、停顿时间。
- 默认开启状态。
六、CMS回收器:低延迟
1. 概述
- 老年代收集器,无法与Parallel Scavenge配合工作。
- JDK 1.5时,适用于强交互应用的并发垃圾收集器Concurrent-Mark-Sweep。
- 是Hotspot第一款真正意义上的并发垃圾收集器,实现用户线程和垃圾回收线程的并发工作。
- 关注点:缩短用户线程停顿时间,达到交互效果。
- 采用标记-清除算法,STW机制。
2. 运行过程
1) 初始标记
- STW机制,停止所有用户线程。
- 标记出GC Roots能直接关联的对象。
- 速度非常快。
2) 并发标记
- 从上一步的GC Roots直接关联的对象开始,遍历整个对象图。
- 耗时较长,与用户线程并发执行。
3) 重新标记
- 修正并发标记期间,因为用户线程继续运作而导致的关系变动的一部分对象。
- 运行时间,相比初始标记阶段稍长。
4) 并发清除
- 清理标记对象,释放内存空间。
- 使用标记-清除(Mark-Sweep)算法,无需移动活对象,所以可以与用户线程并发执行。
3. 分析
- 最耗时的并发标记与并发清除阶段,用户线程不暂停,整体回收是低停顿的。
- 用户线程不中断,要求用户线程有足够的内存可用。
- 回收时机:堆内存达到一定阈值。
- 预留内存不够,就会出现一次【Concurrent Mode Failure】失败。此时采用备选方案:临时启用Serial Old进行老年代GC,停顿时间会更长一些。
- 为什么不用标记-压缩(Mark-Compact)算法?
- 并发清除,保证用户线程执行,不能修改存活对象的地址。
1) 优势
- 并发收集
- 低延迟
2) 劣势
- 产生内存碎片。不能修改存活对象的地址。碎片化严重导致提前触发Full GC。
- 对CPU资源非常敏感。并发阶段,不会停顿用户线程,但会导致应用程序变慢,总吞吐量降低。
- 无法处理浮动垃圾。【并发标记】阶段仅修正【怀疑是垃圾但不是垃圾】的对象。该阶段用户线程产生的垃圾对象不在初始阶段GC Roots中,【变成垃圾】的对象无法处理。
4. 参数设置
- -XX:+UseConcMarkSweepGC
- 手动指定CMS,同时新生代GC自动绑定为ParNew(-XX:+UseParNewGC)。
- -XX:CMSInitiatingOccupanyFraction
- 堆内存达到该阈值时,开始执行GC。
- JDK 1.5默认68,JDK 1.6默认92。
- 该选项可有效降低Full GC次数。
- -XX:+UseCMSCompactAtFullCollection
- 开启Full GC后,对内存空间进行压缩整理。
- -XX:CMSFullGCsBeforeCompaction=m。
- Full GC执行m次后,对内存空间压缩整理。
- -XX:ParallelCMSThreads=n
- 设置CMS线程数。默认 (ParallelGCThreads + 3) / 4。
5. 小结
- 最小化内存,并行开销,选Serial GC
- 最大化应用程序吞吐量,选Parallel GC
- 最小化GC中断时间,选择CMS GC
七、G1回收器:区域化分代式
1. 设计初衷
- 业务越来越庞大、复杂,用户越来越多。
- 适应不断扩大的内存和不断增加的处理器数量。
- 在延迟可控的情况下尽可能提高吞吐量,希望它是一款【全功能收集器】。
2. 命名起源(Garbage First)
- G1是一个并行回收器,使用不同的Region来表示Eden,S0,S1,Old区域。
- 有计划地避免在整个Heap空间全区域垃圾回收。跟踪各个Region垃圾堆积价值大小(回收空间大小和所需时间的经验值),在后台维护一个优先列表。每次根据运行收集的时间,优先回收堆积价值最大的Region。
- 【垃圾优先Garbage First】。侧重于回收垃圾最大量的Region。
3. 概述
- 面向服务端应用。针对配备多核CPU及大容量内存的机器。
- G1在JDK 7开始启用实验标识,JDK 9成为默认。CMS在JDK 9被标识废弃,在JDK 14中移除。
4. 优劣
1) 优势
- 并行与并发。
- 并行性。可以有多个GC线程同时工作,有效利用多核计算能力,此时用户线程
STW。 - 并发性。部分工作可以与应用线程交替执行,不会在整个回收阶段完全阻塞。
- 并行性。可以有多个GC线程同时工作,有效利用多核计算能力,此时用户线程
- 分代收集。
- 堆空间划分为若干区域(约2048块区域),这些区域中包含了逻辑上的年轻代和老年代。物理上可以不连续,通过动态分配实现逻辑上的连续性。
- 在回收后,区域可以更换类型,不用再坚持固定大小和固定数量。
- 兼顾了年轻代和老年代。
- 新增Humongous内存区域,存储大对象(超过1.5个region)
- 避免短期存活的大对象对GC造成父面影响。
- 若H区放不下,则寻找连续的H区。
- 若找不到连续的H区,则执行Full GC。
- 空间整合。两种算法都可以避免内存碎片。
- Region之间是复制算法。
- 整体上是标记-压缩算法。
- 在Java堆比较大的时候,G1优势更明显。
- 可预测停顿时间模型(即软实时,soft real-time)。
- 用户可指定在长度为M毫秒的时间片内,消耗在 GC上的时间不超过N毫秒。
- 分区的设计决定了,G1可以只选取部分区域进行内存回收。
- 根据允许的时间,优先回收价值最大的Region。
- 相较于CMS,G1未必能做到CMS的最好情况的延时停顿,但最差情况要好很多。
2) 劣势
- 不具有压倒性优势。G1的GC内存占用和额外负载都比CMS高。
- G1在大内存上更占优势,CMS在小内存更占优势。平衡点在6~8GB之间。
5. 参数设置
- -XX:+UseG1GC
手动指定G1执行垃圾收集 - -XX:G1HeapRegionSize
设置每个Region的大小。值是2的幂。范围是1MB到32MB之间。目标是根据Java堆大小划分出大约2048个区域。 - -XX:MaxGCPauseMillis
期望的GC停顿时间上限。JVM尽量达到,可能会超过该值。 - -XX:ParallelGCThread
设置STW时GC线程数的值,最多设置为8。 - -XX:ConcGCThread
并发标记线程数。一般为并行垃圾回收线程数(-XX:ParallelGCThread)的 1/ 4 左右。 - -XX:InitiatingHeapOccupancyPercent
设置触发并发GC的堆占用率阈值,默认45。
6. 适用场景
- 面向服务端应用,具有大内存、多处理器的机器。
- 需要GC延迟低的场景。
- 替换CMS收集器的情景。
- 超过50%的Java堆被活动数据占用。
- 对象分配频率或年代提升频率变化很大。
- GC停顿时间过长(长于0.5s至1s)。
- 在并发处理时,G1的GC线程若处理较慢,系统会调用应用程序线程帮助加速GC过程。
7. 回收过程
Remembered Set 解决跨代引用需全区域扫描的问题
回收过程
1) 年轻代GC(Young GC)
1. 概述
- 触发条件
- Eden区快用尽
- 回收类型
- 并行的独占式回收(Parallel + STW)
- 回收操作
- 从年轻代Eden移动对象到Survivor或老年代。
2. 回收过程详述
- 第一阶段:扫描GC Roots。
- 类变量,当前方法栈的局部变量表等。
- 第二阶段,更新RSet。
- 处理dirty card queue中的card,更新RSet。
- dirty card queue是一个缓存引用关系的队列。如果在引用赋值的时候就更新RSet,需要线程间同步,无疑会极大降低应用线程的性能。队列的性能会好很多。
- 该过程后,RSet可以准确反映老年代对所在内存分段中的引用。
- 第三阶段,处理RSet。
- 识别被老年代对象指向的Eden中的对象。这些被指向的Eden中的对象可以认为是存活的对象。
- 第四阶段,复制对象。
- 采用复制算法处理年轻代,复制到Survivor或老年代。
- 第五阶段,处理引用。
- 处理Soft,Weak,Phantom,Final,JNI等引用。
最终Eden数据为空,GC停止工作,无碎片空间。
2) 老年代并发标记(Concurrent Marking)
1. 触发条件
- 堆空间达到一定使用率(默认45%)
2. 回收过程
- 初始标记。与CMS该部分一致。
- 标记从根结点直接可达的对象。
- STW,触发一次Young GC。
- 根区域扫描(Root Region Scanning)。
- 扫描Survivor区直接可达的老年代对象,并标记被引用的对象。
- 该过程必须在Young GC之前完成。
- 并发标记(Concurrent Marking)。
- 整个堆空间进行标记,该过程可能被Young GC中断。
- 若发现区域中所有对象都是垃圾,则这个区域会立即被回收。
- 会计算每个区域的存活对象比例(对象活性)。
- 再次标记(Remark)。
- 修正并发标记的结果。是STW的过程。
- 采用了初始快照算法(snapshot-at-the-beginning),比CMS更快。
- 独占清理(cleanup,STW)。
- 计算各个区域存活对象和GC回收比例。
- 为混合回收做铺垫。
- 并发清理阶段。
- 识别并清理完全空闲的区域。
3) 混合回收(Mixed GC)
- 年轻代和老年代的混合回收。
- 年轻代回收全部。
- 老年代选取一部分(按分配的GC时间片和Region的回收时间)进行。
- 老年代并发标记结束后,
- 老年代中百分百为垃圾的Region的内存分段被立即回收。
- 部分为垃圾的Region的内存分段被计算出来。
- 默认情况下,这些老年代的内存分段会分为8次被回收(VM参数设置:-XX:G1MixedGcCountTarget)。
- 混合回收的回收集,包括1 / 8 的老年代内存分段,eden区内存分段全部,Survivor区内存分段全部。
- 混合回收的算法和年轻代回收算法完全一致,只是回收集多了老年代的内存分段。
- 由于老年代中内存分段默认分8次回收,G1优先回收垃圾多的内存分段。
- 阈值设置:-XX:G1MixedGcLiveThresholdPercent。默认65%,垃圾占比达到该值才会回收。
- 混合回收不一定进行8次。
- 阈值设置-XX:G1HeapWastePercent,默认10%。允许堆内存中有10%的空间被浪费。如果发现可回收的内存低于堆内存10%,则不再进行混合回收。
4) Full GC(可能需要)
G1的初衷即是避免Full GC的发生,因为Full GC伴随着STW,且单线程,性能会很差,停顿时间很长。
Full GC出现的可能原因:
- Evacuation(回收)的时候没有足够的to-space来存放晋升的对象。
- 并发处理之前空间耗尽。
8. 补充
1) 官方设计构想
Oracle官方曾计划,回收阶段设计为与用户程序一起并发执行,但很复杂。
- 考虑G1之回收部分Region,停顿时间用户可控,所以并不迫切实现,而将这个特性放到低延迟垃圾收集器ZGC中。
- 同时G1不仅面向低延迟,停顿用户线程能最大幅度提高垃圾回收效率,保证吞吐量。
2) 优化建议
- 年轻代大小:
- 避免-Xmn或者-XX:NewRatio等相关选项显式设置年轻代大小。
- 固定年轻代大小会覆盖暂停时间的目标。
- 暂停时间目标,不宜太苛刻。
- G1吞吐量目标是应用程序占90%。GC占10%;
- 暂停时间太苛刻,表示你愿意承受更多的垃圾回收开销,直接影响吞吐量。
八、垃圾回收器小结
1. 表格概况
垃圾收集器 | 分类 | 作用位置 | 算法 | 特点 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制 | 响应速度优先 | 单CPU环境的Client模式 |
ParNew | 并行 | 新生代 | 复制 | 响应速度优先 | 多CPU环境Server模式,与CMS配合 |
Parallel | 并行 | 新生代 | 复制 | 吞吐量优先 | 后台运算,不需太多交互场景 |
Serial Old | 串行 | 老年代 | 标记-压缩 | 响应速度优先 | 单CPU环境的client模式 |
Parallel Old | 并行 | 老年代 | 标记-压缩 | 吞吐量优先 | 后台运算,不需太多交互的场景 |
CMS | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 适用于互联网或B / S 业务 |
G1 | 并发 + 并行 | 新生代 + 老年代 | 1. 标记-压缩 2. 复制 |
响应速度优先 | 面向服务端应用 |
2. GC发展阶段
Serial => Parallel(并行) => CMS(并发) => G1 => ZGC
3. 垃圾回收器组合
3. 如何选择垃圾收集器
- 优先调整堆大小,由JVM自适应完成。
- 如果内存小于100M,串行收集器。
- 单核、单机程序,并且没有停顿时间要求,串行收集器。
- 如果是多CPU,需要高吞吐量,允许停顿时间超过1s,并行或让JVM自行选择。
- 如果是多CPU、追求低停顿时间,需快速响应(比如延迟不超过1s,互联网),使用并发收集器(官方推荐G1,互联网项目首选)。
4. 面试
垃圾收集部分,面试官可以循序渐进从理论、实践、各种角度深入,未必要求面试者都懂。如果你懂得原理,一定会成为面试中的加分项。
比较通用、基础的部分如下:
- 垃圾收集算法有哪些?如何判断一个对象是否可以被回收。
- 垃圾收集器工作的基本流程。
另外,需多关注垃圾收集器中常用的参数。
九、GC日志分析
1. 内存分配与垃圾回收参数列表
参数名 | 参数说明 | 图片例子 |
---|---|---|
-XX:+PrintGC | 输出GC日志。类似于-verbose:gc | |
-XX:+PrintGCDetails | 输出GC详细日志 | |
-XX:+PrintGCTimeStamps-XX:+PrintGCDateStamps | 1. 输出GC的时间戳 2. 输出GC的时间,以yyyy-MM-ddThh:mm:ss形式 |
|
-XX:+PrintHeapAtGC | 进行GC前后,打印堆信息 | |
-Xloggc:../logs/gc.log | 定义日志文件的输出路径 |
2. 补充说明
- [GC和[Full GC说明了这次垃圾收集的停顿类型,如果有Full则说明GC发生了STW;
- 使用Serial收集器在新生代的名字是Default New Generation,因此显示的是[DefNew;
- 使用ParNew收集器在新生代的名字会变成[ParNew,意思是Parallel New Generation;
- 使用Parallel Scavenge收集器在新生代的名字是[PSYoungGen;
- 使用G1收集器的话,会显示为garbage-first heap;
- Allocation Failure
- 表示引起GC原因是,年轻代空间不足以分配。
- [PsYoungGen: 5986K->696K(8704K)]5986K->704K(9216K)
- 中括号内:GC回收前年轻代大小,回收后大小,(年轻代总大小)
- 括号外:GC回收前年轻代和老年代大小,回收后大小,(年轻代和老年代总大小)
- user代表用户态回收耗时,sys内核态回收耗时,real实际耗时。由于多核的原因,时间总和可能会超过real时间。
3. 分代说明
1) Minor GC
2) Full GC
4. 代码详细分析
1) 示例代码
/**
* GC日志文件位置测试:<p> 
* 在 JDK 7 和 JDK 8 中分别执行。<p>
* 参数列表:<p> 
* -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
*
* @author Jinhua
* @version 1.0
* @date 2021/5/12 22:55
*/
public class GcLogAllocation {
private static final int MB = 1024 * 1024;
public static void main(String[] args) {
testAllocation();
}
@SuppressWarnings("all")
public static void testAllocation() {
byte[] bytes1, bytes2, bytes3, bytes4;
bytes1 = new byte[2 * MB];
bytes2 = new byte[2 * MB];
bytes3 = new byte[2 * MB];
bytes4 = new byte[4 * MB];
}
}
2) 分析
3) 不同结果
- JDK 7
- JDK 8
- 对比数值上,JDK 8 –> 大对象直接进入老年代
5. GC日志分析工具
- GCViewer
- GCEasy
- GCHisto
- GCLogViewer
- Hpjmeter
- garbagecat
十、垃圾回收器的新发展
1. 发展概述
JDK 11
- Epsilon GC
- 无操作(no-op),内存分配完成直接退出。
- ZGC
- 可伸缩低延迟
Open JDK 12
- Shenandoah GC(实验性)
- 第一款不是Oracle团队开发的。受到官方排挤。
- 低停顿时间。
- 暂停时间与堆大小无关。无论是200M还是200G,都可以把停顿时间限制到10ms以内。
2. Shenandoah GC
1) 开发团队的测试数据分析
收集器 | 运行时间 | 总停顿 | 最大停顿 | 平均停顿 |
---|---|---|---|---|
Shenandoah | 387.602s | 320ms | 89.79s | 53.01ms |
G1 | 312.052s | 11.7s | 1.24s | 450.12ms |
CMS | 285.264s | 12.78s | 4.39s | 852.26ms |
Parallel Scavenge | 260.092s | 6.59s | 3.04s | 823.75ms |
- 结果是RedHat在2016年发表的论文数据,测试内容是使用ES对200G维基百科数据进行索引。从结果看:
- 停顿时间有了质的飞跃。
- 吞吐量方面有明显下降。
- 总运行时间是最长的。
2) 小结
a. 优势
- 低延迟时间。
b. 劣势
- 高运行负担下吞吐量下降。
3. 革命性的ZGC
- JDK 14之前,仅支持Linux。
- 目前mac或Windows也可以使用了。
- -XX:+UnlockExperimentalVMOptions -XX:+UseZGC
1) 概述
a. 目标
尽可能对吞吐量影响不大的前提下,实现任意堆内存大小下都可以把垃圾收集器的停顿时间限制在10ms内。
b. 技术实现
基于Region内存布局,暂不设分代,使用读屏障、染色指针、内存多重映射等技术实现可并发的标记-压缩算法。
c. 过程概述
除了初始阶段是STW的,其余几乎所有地方都是并发执行的,主要分为四个阶段:
- 并发标记;
- 并发预备重分配;
- 并发重分配;
- 并发重映射。
2) 官方测试数据
a. 延迟与吞吐量对比
b. 停顿时间对比
4. 阿里巴巴AliGC
1) 概述
- 基于G1的算法。
- 面向大堆(Large Heap)应用场景。