1.GC分类和性能指标
1.1 垃圾回收器分类
- 按线程数:这里的线程数指的用于垃圾回收的线程数,且两者都会STW
- 串行回收器:单CPU上性能较好,默认被应用在客户端的JVM中
- 并行回收器:在并发能力强的CPU上,停顿时间短
-
按工作模式:
- 并发式垃圾回收器:与用户线程交替执行,减少程序的停顿时间
- 独占式垃圾回收器:一运行就停止程序中所有用户线程,直到垃圾回收完成
-
按碎片处理方法:
- 压缩式垃圾回收器:回收完后对存活对象进行压缩整理,消除回收后的碎片
- 非压缩式垃圾回收器:回收完后对存活对象不进行压缩操作
-
按工作的内存区间:
- 年轻代垃圾回收器
- 老年代垃圾回收器
1.2 评估指标
在最大吞吐量优先的情况下,降低暂停时间
- 吞吐量:运行用户代码时间占总运行时间的比例(高吞吐量意味着程序运行越快,需要降低内存回收的执行频率,导致需要更长的暂停时间执行内存回收)
- 垃圾收集开销:垃圾收集需要时间占总运行时间的比例
- 暂停时间:垃圾回收时,工作线程被暂停的时间(低暂停时间意味着低延迟,需要频繁进行内存回收,导致垃圾收集的总时间变大,吞吐量下降)
- 收集频率:相当于应用程序的执行,收集操作发生的频率
- 内存占用:堆区占用内存大小(内存越大意味着一次执行垃圾收集的时间越长,即延迟越大)
- 快速:一个对象从诞生到被回收经历的时间
2.不同垃圾回收器概述
2.1 7个经典垃圾收集器
- 串行回收器:Serial(第一款GC)、Serial Old
- 并行回收器:ParNew(Serial多线程版本)、Parallel Scavenge、Parallel Old(JDK8默认使用Parallel Scavenge+Parallel Old)
- 并发回收器:CMS、G1(JDK9默认)
2.2 7个垃圾收集器和垃圾分代间的关系
2.3 垃圾收集器的组合关系
连线表示组合关系,虚拟表示废弃的组合或收集器
3.Serial回收器-串行回收
3.1 Serial
-
客户端模式下默认的新生代垃圾收集器(因为它在单CPU下性能不错)
-
采用复制算法
-
串行回收且使用STW机制
3.2 Serial Old
-
采用标记压缩算法
-
串行回收且使用STW机制
-
客户端模式下默认的老年代垃圾收集器(因为它在单CPU下性能不错)
-
在服务端主要有两种用途:
- 与新生代的Parallel Scavenge配合使用
- 作为老年代CMS的后备垃圾收集方案
3.3 优缺点
- 优点
- 简单且高效
- 没有线程交互的开销(该收集器限定在单CPU)
- 运行在客户端不错(因为客户端内存不大,单线程可在较短时间内完成垃圾收集)
- 缺点:
- 该收集器在多核效率不高(现在机器都是多核)
- 不适用交互性较强的应用
-XX:+UserSerialGC
可以指定年轻代和老年代都使用Serial收集器
4.ParNew回收器-并行回收
-
Serial收集器的多线程版本
-
只能处理新生代
-
采用复制算法
-
并行回收且使用STW机制
-
很多JVM在服务端模式下默认的新生代垃圾收集器
对于新生代,由于回收次数频繁,所以使用并行方式高效;对于老年代,由于回收次数少,所以使用串行方式节省资源(多线程需要切换,消耗资源)。ParNew在新生代是并行回收,是不是就一定比Serial收集器高效?No!要分在单CPU环境还是多CPU环境
- 在多CPU环境下,ParNew可以充分利用多CPU,快速完成收集,提升吞吐量
- 在单CPU环境下,ParNew并不比Serial高效,因为Serial不需要切换线程,避免额外开销
-XX:+UserParNewGC
可以指定年轻代使用ParNew
-XX:ParallelGCThreads
限制线程数量
5.Parallel回收器-吞吐量优先
5.1 Parallel Scavenge
在年轻代中ParNew收集器已经基于并行回收了,为什么还要Parallel Scavenge收集器?
- 与ParNew不同,Parallel Scavenge目标达到可控制的吞吐量
- 与ParNew不同,Parallel Scavenge存在自适应调节策略(年轻代的大小、Eden区和Survivor的比例等参数会被自动调整,达到堆大小、吞吐量和停顿时间间的平衡点)
- 采用复制算法
- 并行回收且使用STW机制
- JDK8默认的新生代收集器
高吞吐量可以高效利用CPU时间,尽快完成程序的运算任务,适合在后台计算且不需要太多交互的任务(如订单处理等)
5.2 Parallel Old
- 采用标记压缩算法
- 并行回收且使用STW机制
- JDK8默认的老年代收集器
-XX:+UserParallelGC
可以指定年轻代使用Parallel Scavenge;-XX:+UserParallelOldGC
可以指定老年代使用Parallel Old(这两个参数一个开启,默认另一个也会被开启)
-XX:ParallelGCThreads
限制线程数量
6.CMS回收器-低延迟
CMS:Concurrent-Mark-Sweep,JDK14已经被删除
6.1 四个阶段
CMS整个过程分为四个阶段:
-
初始标记阶段:用户线程会STW,垃圾回收线程只是标记出
GC Roots
能直接关联的对象,标记完就恢复线程,因为直接关联对象少,所以该阶段执行速度快 -
并发标记:从
GC Roots
的直接关联对象开始遍历所有对象,耗时较长但是不需要暂停用户线程 -
重新标记:在并发标记阶段,由于用户线程没有暂停所以可能会出现对象的变动。该阶段修正那部分变动对象的标记记录(被修正的变动对象是值原来是垃圾但是变更为非垃圾的对象,并不是因为用户线程执行由非垃圾而变为垃圾的对象),相较于并发标记阶段时间短,但是用户线程需要STW
-
并发清除:清理标记阶段判断已死亡的对象,释放内存空间。由于不需要移动存活对象,所以该阶段可以与用户线程并发执行(不需要STW)
6.1 优点
- HotSpot中第一款真正意义的并发收集器(即第一次实现让垃圾收集线程和用户线程同时工作)
- 尽可能缩短垃圾收集时间(在初始标记和重新标记阶段还是存在短暂的STW),即低延迟
- 采用标记清除算法
- 并发回收且使用STW机制
- 在老年代中使用
低延迟适用于与用户交互的程序,比如B/S系统的服务端与网站
6.2 缺点
- 会产生内存碎片(若干次GC后才进行一次碎片整理),导致会出现无法分配大对象的情况下触发
Full GC
- 总吞吐量降低(垃圾收集线程和用户线程并发执行相当于占用了部分用户线程,即对CPU资源敏感)
- 无法处理浮动垃圾(并发标记阶段如果产生新的垃圾对象,CMS无法对这些垃圾对象进行标记,这些对象不会被及时回收,只能等下次GC)
由于在CMS垃圾收集中用户线程没有中断,所以要确保用户线程由足够内存可以使用,所以CMS不能等老年代完全被填满再收集,而是等堆内存使用率达到阈值后开始回收。
如果预留内存无法满足用户线程使用,则出现
Concurrent Mode Failure
,JVM会临时启动Serial Old重新进行老年代收集(用户线程停顿时间就变长了)
标记清除算法会产生内存碎片,为什么不使用标记压缩算法?
采用压缩算法整理内存时,对象地址会被修改,而用户线程还在运行,其使用的内存被压缩整理后无法使用
-XX:+UserConcMarkSweepGC
可以指定老年代使用CMS,开启后会自动开启ParNew
7.G1回收器-区域化分代式
7.1 介绍
-
目标是在延迟可控(即低的STW时间)的情况下获得尽可能高的吞吐量
-
把堆划分为不相关的区域Region(物理上不连续),用于表示Eden区、survivor0、survivor1、老年代等
-
进行全区域的垃圾回收
-
每次根据允许的收集时间,优先回收价值较大的Region(Garbage First命名由来):
- 它会跟踪每个Region中垃圾堆积的价值(即回收后获得空间大小和需要时间的经验值),并在后台维护优先列表
-
针对多核CPU和大容量内存的机器(如服务端应用)
-
JDK9后默认垃圾回收器
-XX:+UseG1GC:指定G1收集器
-XX:G1HeapRegionSize:设置Region的大小
-XX:MaxGCPauseMillis:设置期望达到的最大GC停顿时间指标
7.2 优点
- 并行和并发:
- 并行性:G1可以有多个GC线程同时工作,当然用户线程还是会STW
- 并发性:G1的部分工作可以和应用程序同时执行,即不会在整个回收阶段完全阻塞应用程序
- 分代收集:
- 依旧属于分代型垃圾回收器,只不过从堆结构上不要求Eden区、survivor区和老年代是连续的,也不固定大小和数量
- 同时兼顾了年轻代和老年代
- 将堆空间分为若干区域,这些区域包含了逻辑上的年轻代和老年代
- 空间整合:
- G1将内存划分为多个Region,回收时以Region为单位
- Region之间采用复制算法,但是从整体上看做标记压缩算法(避免了内存碎片,堆越多G1的优势越明显)
- 可预测的停顿时间模型:即能让使用者指定在M毫秒的时间片内,消耗垃圾收集上的时间不超过N毫秒
- G1可只选取部分区域进行回收(控制了回收时间)
- 每次根据允许收集的时间,优先回收价值较大的Region,保证了在有限时间内较高的收集效率
7.3 缺点
- 相较于CMS,G1在GC时占用的内存和负载都要高
- 在小内存应用上CMS表现大概率优于G1
7.4 什么是Region?
- 所有Region大小一样,为2的幂次方
- 一个Region可能属于Eden(E)、Survivor(S)或Old(O)区域中的某个角色(当然被回收后可以成为其他角色),同时加入了Humongous(H)区,用于存储大对象(对象超过了1.5个Region就放到H)
为什么要设置H?对于堆中的大对象默认直接放到老年代,但如果它只是短期存在的大对象,而老年代回收频率低,相当于内存泄漏(该对象要被收集却因为清除频率低未被收集),所以使用H区装大对象(如果一个H区装不下就找连续的H区,找不到连续的H区就Full GC)
7.5 垃圾回收过程
- 年轻代GC:使用并行(多个垃圾回收线程)独占式(出现STW)收集器
- 老年代并发标记:堆内存使用达到一定值(默认45%)开始老年代并发标记过程
- 混合回收:包括年轻代GC和老年代GC
7.5.1 什么是Remembered Set?
一个Region对象可能会被其他Region引用,意味着回收年轻代时还需要扫描老年代(即对整个堆进行扫描,相当于Full GC了),降低了GC效率,如何避免这种扫描?
-
每个Region都有对应的一个
Remembered Set
-
对每个引用类型数据进行写操作时(即引用其他对象),会产生一个写屏障暂停中断,然后检查要引用的对象是否和引用类型数据在同一个Region中:
- 不在同一个则通过
CardTable
把引用信息记录到引用类型对象所在的Region的Remembered Set
(记录这个Region清理时需要扫描哪几个Region,判断其他Region是否还引用它)
- 不在同一个则通过
-
垃圾收集时,把
Remembered Set
加入到GC Roots
(回收年轻代时,老年代的对象相当于GC Roots
)的枚举范围中
7.5.2 年轻代GC
-
首先G1停止应用程序,并且创建回收集(需要被回收的内存分段集合,年轻代GC时该集合包括所有Eden区和Survivor区)
-
然后开始以下过程:
- 扫描GC Roots和
Remembered Set
引用的对象(避免全堆扫描) - 更新
Remembered Set
,保证Remembered Set
- 处理
Remembered Set
- 复制对象(即把Eden区存活的对象复制到Survivor区空的内存段)
- 处理引用
- 扫描GC Roots和
7.5.3 并发标记过程
- 初始标记阶段
- 根区域扫描
- 并发标记
- 再次标记
- 独占清理
- 并发处理阶段
7.5.4 混合回收
回收全部年轻代和部分老年代
总结
不同垃圾收集器的区别
如何选择垃圾回收器?
- 优先调整堆的大小,让JVM自适应完成
- 如果内存小于100M,使用串行收集器
- 如果是单核/单机程序且没有停顿时间要求,使用串行收集器
- 如果是多CPU、需要高吞吐量且允许停顿时间超过1S,使用并行收集器
- 如果是多CPU、追求低停顿时间(即低延迟),使用并发收集器