目录
垃圾收集器
评估GC的性能指标
- 吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)
- 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
- 收集频率:相对于应用程序的执行,收集操作发生的频率。
- 内存占用:Java堆区所占的内存大小。
- 快速:一个对象从诞生到被回收所经历的时间。
吞吐量、暂停时间、内存占用 这三者共同构成一个“不可能三角”,主要是吞吐量和暂停时间
性能指标:吞吐量
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间)
性能指标:暂停时间
“暂停时间”是指一个时间段内应用程序线程暂停,让GC线程执行的状态
”高吞吐量”和”低暂停时间”是一对相互竞争的目标
现在标准:在最大吞吐量优先的情况下,降低停顿时间
垃圾回收器
垃圾回收器发展史
有了虚拟机,就一定需要收集垃圾的机制,这就是Garbage Collection,对应的产品我们称为Garbage Collector。
- 1999年随JDK1.3.1一起来的是串行方式的serialGc,它是第一款GC。ParNew垃圾收集器是Serial收集器的多线程版本
- 2002年2月26日,Parallel GC和Concurrent Mark Sweep GC跟随JDK1.4.2Z一起发布·
- Parallel GC在JDK6之后成为HotSpot默认GC。
- 2012年,在JDK1.7u4版本中,G1可用。
- 2017年,JDK9中G1变成默认的垃圾收集器,以替代CMS。
- 2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟。
- 2018年9月,JDK11发布。引入Epsilon 垃圾回收器,又被称为 "No-Op(无操作)“ 回收器。同时,引入ZGC:可伸缩的低延迟垃圾回收器(Experimental)
- 2019年3月,JDK12发布。增强G1,自动返回未用堆内存给操作系统。同时,引入Shenandoah GC:低停顿时间的GC(Experimental)。·2019年9月,JDK13发布。增强ZGC,自动返回未用堆内存给操作系统。
- 2020年3月,JDK14发布。删除CMS垃圾回收器。扩展ZGC在macos和Windows上的应用
7种经典的垃圾收集器
串行回收器:Serial、Serial old
并行回收器:ParNew、Parallel Scavenge、Parallel old
并发回收器:CMS、G11
与垃圾分代之间的关系
垃圾收集器的组合关系
- 两个收集器间有连线,表明它们可以搭配使用:Serial/Serial old、Serial/CMS、ParNew/Serial old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
- 其中Serial old作为CMs出现"Concurrent Mode Failure"失败的后备预案。
- (红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、ParNew+Serial old这两个组合声明为废弃(JEP173),并在JDK9中完全取消了这些组合的支持(JEP214),即:移除。
- (绿色虚线)JDK14中:弃用Parallel Scavenge和Serial old GC组合(JEP366)
- (青色虚线)JDK14中:删除CMS垃圾回收器(JEP363)
需要针对不同的场景,提供不同的垃圾收集器,提高垃圾收集的性能。
Serial/Serial Old回收器:串行回收
[ˈsɪriəl]连续的
Serial
- 新生代
- 最基础,历史最悠久
- 采用复制算法、串行回收和STW机制
- 单线程工作(只会使用一个处理器或一条收集线程去完成垃圾收集工作,在收集时要暂停所有线程)
- 简单而高效,额外内存消耗最小
- 客户端下的HotSpot默认的新生代垃圾收集器
Serial收集器还提供用于执行老年代垃圾收集的Serial old收集器
- 标记-整理算法、串行回收和STW机制
- 是运行在客户端模式下默认的老年代的垃圾回收器
- 在服务端与新生代的Parallel scavenge配合使用、作为老年代CMS收集器的后备垃圾收集方案
Serial/Serial Old收集器运行示意图
ParNew收集器:并行回收
- 新生代
- 实质上是Serial的多线程并行版本
- 采用复制算法、STW机制
- JDK9之后只能和CMS搭配使用
ParNew/Serial Old收集器运行示意图
- 对于新生代,回收次数频繁,使用并行方式高效。
- 对于老年代,回收次数少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源)
Parallel Scavenge/Parallel Old收集器:吞吐量优先
['pærəlel] 平行 [ˈskævɪndʒ]清除
Parallel Scavenge
- 新生代
- 标记-复制算法
- 并行收集
- 目标是达到一个可控制的吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)
- 自适应调节策略
Parallel Old
-
多线程并行收集
-
标记-整理算法
-
吞吐量优先的组合
-
-XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间,是一个大于零的毫秒数,牺牲吞吐量和新生代空间换取
-
-XX:GCTimeRatio:直接设置吞吐量大小,大于0小于100,垃圾收集时间占总时间的比率
-
-XX:+UseAdaptiveSizePolicy自适应调节策略
CMS收集器:低延迟
Concurrent Mark Sweep
-
以获取最短回收停顿时间为目标,实现了让垃圾收集线程与用户线程同时工作
-
基于标记-清除算法
-
过程
1)初始标记:STW,标记GC Roots能直接关联到的对象,速度很快
2)并发标记:从GC Roots的直接关联对象开始遍历整个对象图,耗时长,不需要停顿用户线程,与垃圾收集器并发运行
3)重新标记:STW,修正并发标记阶段因用户线程运行导致标记变动的对象的标记记录
4)并发清除:清理删除掉标记阶段判断的已经死亡的对象,不需要停顿用户线程 -
从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的
优点: -
并发、低延迟
缺点:
- 1.对处理器资源非常敏感:并发
- 2.无法处理运行时用户线程产生的“浮点垃圾”,Concurrent Mode Failure,导致Full GC
- 3.标记-清除算法产生的空间碎片
Garbage First(G1)收集器:区域化分代式
-
在延迟可控的情况下获得尽可能高的吞吐量,全功能的垃圾收集器
-
开创了收集器面向局部收集的设计思路和基于Region的内存布局形式
-
主要面向服务端应用
-
可以由用户设定停顿时间,一般为两百毫秒
-
面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,面向哪块内存中存放的垃圾数量最多,回收收益最大。Mixed GC
-
Humongous区域,专门用来存储大对象
-
每个Region都维护自己的记忆集,本质上是个哈希表,处理Region之间的引用,避免全堆扫描
-
并发解决方案:原始快照SATB实现
-
并发回收过程中创建新对象:两个TAMS(Top at Mark Start)指针,新分配的对象地址在这两个指针之上,默认存活
G1优点
并行与并发
- 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
- 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
分代收集
- 把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。
- 同时兼顾新生代和老年代
空间整合
- 内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片
可预测的停顿时间模型(即:软实时soft real-time)
-
建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
-
G1可以只选取部分区域进行内存回收,对于全局停顿情况的发生也能得到较好的控制
-
G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
G1缺点
- 缺点:为了垃圾收集产生的内存占用大,额外执行负载高
参数设置
- -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。
G1收集器的常见操作步骤
- 第一步:开启G1垃圾收集器
- 第二步:设置堆的最大内存
- 第三步:设置最大的停顿时间
提供了三种垃圾回收模式:YoungGC、Mixed GC和Full GC,在不同的条件下被触发
G1收集器的适用场景
面向服务端应用,针对具有大内存、多处理器的机器。
需要低GC延迟,并具有大堆的应用程序提供解决方案
分区Region
它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB,2MB,4MB,8MB,16MB,32MB
XX:G1HeapRegionsize 设定,所有的Region大小相同,且在JVM生命周期内不会被改变。
通过指针碰撞分配空间
Humongous内存区域
主要用于存储大对象,如果超过1.5个region,就放到H
对于堆中的对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Fu11Gc。G1的大多数行为都把H区作为老年代的一部分来看待。
收集过程
1.初始标记:标记GC Roots能直接关联到的对象,修改TAMS(将region中的一部分空间划分出来用于并发回收的对象分配)
2.并发标记: 从GC Roots的直接关联对象开始遍历整个对象图,耗时长,与用户线程并发。
3.最终标记:暂停用户线程,处理遗留SATB(原始快照)记录
4.筛选回收:更新Region数据,对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间进行回收。暂停用户线程
垃圾收集器小结
3.6 低延迟垃圾收集器
垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency),三者共同构成了一个“不可能三角”
Shenandoah和ZGC
- 只有初始标记、最终标记有短暂的停顿,且时间基本固定
Shenandoah收集器
- 与G1类似,但使用连接矩阵来记录跨Region引用
1.初始标记
2.并发标记
3.最终标记
4.并发清理
5.并发回收
6.初始引用更新
7.并发引用更新
8.最终引用更新
9.并发清理
ZGC收集器
- 动态的Region
- 染色指针计数:将少量额外信息存储在指针上
- 管理内存不超过4TB(2的42次幂)
1.并发标记
2.并发预备重分配
3.并发重分配
4.并发重映射