垃圾回收器

垃圾回收器概述

1、垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的 JVM 来实现

2、Java 已经衍生了众多 GC 版本

 

垃圾收集器分类

1、按线程数分:串行垃圾回收器、并行垃圾回收器

2、串行回收器

(1)在同一时间段内只允许有一个 CPU 用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束

(2)在单 CPU 处理器,或较小的应用内存等硬件平台不是特别优越的场景,串行回收器的性能表现可以超过并行回收器、并发回收器

(3)所以,串行回收默认被应用在客户端的 Client 模式下的 JVM 中

3、并行回收器

(1)在并发能力比较强的 CPU 上,并行回收器产生的停顿时间,要短于串行回收器

(2)和串行回收相反,并行收集可以运用多个 CPU 同时执行垃圾回收,因此提升应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用 Stop-the-World 机制

4、按照工作模式分

(1)并发式垃圾回收器:与应用程序线程交替工作,尽可能减少应用程序的停顿时间

(2)独占式垃圾回收器:Stop the world 一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束

5、按碎片处理方式分

(1)压缩式垃圾回收器:在回收完成后,对存活对象进行压缩整理,消除回收后的碎片

(2)非压缩式的垃圾回收器

6、按工作的内存区间分

(1)年轻代垃圾回收器

(2)老年代垃圾回收器

 

评估 GC 的性能指标

1、吞吐量:运行用户代码的时间,占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)

2、垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例

3、暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间

4、收集频率:相对于应用程序的执行,收集操作发生的频率

5、内存占用:Java 堆区所占的内存大小

6、快速:一个对象从诞生到被回收所经历的时间

7、重点:吞吐量、暂停时间

(1)吞吐量、暂停时间、内存占用,三者总体的表现会随着技术进步而越来越好,一款优秀的收集器通常最多同时满足其中的两项

(2)随着硬件发展,暂停时间越来越重要,对内存占用多的容忍度越来越高,有助于降低收集器运行时对应用程序的影响,即提高吞吐量

(3)扩大内存,增加暂停时间,对延迟带来负面效果

 

吞吐量

1、CPU 运行用户代码的时间,与 CPU 总消耗时间的比值

2、吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)

3、吞吐量优先

(1)表示在单位时间内,STW 的时间最短

(2)这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,不必考虑暂停时间(快速响应)

 

暂停时间

1、一个时间段内应用程序线程暂停,让 GC 线程执行的状态

2、暂停时间优先:表示尽可能让单次 STW 的时间最短

 

吞吐量、暂停时间

1、高吞吐量:终端用户感觉只有应用程序线程在做生产性工作,直觉上,吞吐量越高程序运行越快

2、低暂停时间(低延迟):取决于应用程序的类型,对于一个交互式应用程序,低暂停时间是非常重要的

3、高吞吐量、低暂停时间是一对相互竞争的目标(矛盾)

(1)如果选择以吞吐量优先,必需降低内存回收的执行频率,但会导致 GC 需要更长的暂停时间来执行内存回收

(2)如果选择以低延迟优先,只能频繁地执行内存回收,但会引起年轻代内存的缩减和导致程序吞吐量的下降

4、目前标准:在最大吞吐量优先的情况下,降低暂停时间

 

7 种经典的垃圾收集器

1、类别

(1)串行回收器:Serial、Serial Old

(2)并行回收器:ParNew、Parallel Scavenge、Parallel Old

(3)并发回收器:CMS、G1

2、与垃圾分代之间的关系

(1)新生代收集器:Serial、ParNew、Parallel Scavenge

(2)老年代收集器:Serial Old、Parallel Old、CMS

(3)整堆收集器:G1

3、垃圾收集器的组合关系

(1)两个收集器间有连线,表明它们可以搭配使用:Serial + Serial Old、Serial + CMS(Serial Old 备用)、ParNew + Serial Old、ParNew + CMS(Serial Old 备用)、Parallel Scavenge + Serial Old、Parallel Scavenge + Parallel Old、G1

(2)其中 Serial Old 作为 CMS 出现 Concurrent Mode Failure 失败的后备预案

(3)红色虚线:由于维护和兼容性测试的成本,在 JDK 8 时将 Serial + CMS、ParNew + Serial Old 这两个组合声明为废弃(JEP173),并在 JDK9 中完全移除这些组合的支持(JEP214)

(4)绿色虚线:JDK14 中:弃用 Parallel Scavenge + Serialold GC 组合(JEP366)

(5)绿色虚框:JDK14 中:删除 CMS 垃圾回收器(JEP363)

4、查看默认垃圾收集器

(1)-XX:+PrintCommandLineFlags:查看命令行相关参数,包含使用的垃圾收集器

(2)使用命令行指令查看指定 GC:jinfo -flag 相关垃圾回收器参数 进程ID

 

Serial 回收器

1、单线程的收集器,串行回收

2、最基本的垃圾收集器,JDK 1.3 之前回收新生代唯一的选择

(1)HotSpot 中 Client 模式下,默认新生代垃圾收集器

(2)采用复制算法、串行回收、Stop-The-World 机制,执行内存回收

3、优点

(1)限定单个 CPU 环境下,简单、高效,没有线程交互的开销

(2)在用户的桌面应用场景中,可用内存不大,可在较短时间内完成垃圾收集,只要不频繁发生,使用串行回收器是可以接受的

4、缺点

(1)对于强交互应用,串行回收是不能接受的

(2)在 Java Web 应用程序中,不会采用串行垃圾收集器

5、HotSpot 虚拟机设置

(1)指定年轻代、老年代都使用串行收集器:-XX:+UseSerialGC

(2)等价于新生代使用 Serial GC,且老年代使用 Serial Old GC

 

ParNew 回收器

1、并行回收

2、ParNew 收集器是 Serial 收集器的多线程版本

(1)Par:Parallel 的缩写

(2)New:只能处理的是新生代

(3)除了并行回收内存外,两款垃圾收集器之间几乎没有任何区别

(4)在年轻代中同样采用复制算法、Stop-The-World 机制

3、在多数 JVM 运行在 Server 模式下,新生代的默认垃圾收集器

(1)对于新生代,回收次数频繁,使用并行方式高效

(2)对于老年代,下图使用 Serial Old 回收器

(3)在多个 CPU 的环境下,由于可以充分利用多 CPU、多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量

(4)在单个 CPU 的环境下,ParNew 收集器不比 Serial 收集器更高效,由于 CPU 需要频繁地做任务切换,在多线程交互过程中产生的一些额外开销

4、除 Serial 外,目前只有 ParNew GC 能与 CMS 收集器配合工作

5、设置

(1)-XX:+UseParNewGC:手动指定使用 ParNew 收集器执行内存回收任务,表示年轻代使用并行收集器,不影响老年代

(2)-XX:ParallelGCThreads:限制线程数量,默认开启和 CPU 数据相同的线程数

 

Parallel Scavenge 回收器

1、吞吐量优先

2、HotSpot 年轻代中,除了 ParNew 收集器基于并行回收,Parallel Scavenge 收集器也采用复制算法、并行回收、Stop the World 机制

(1)和 ParNew 收集器不同,Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量

(2)Parallel Scavenge、ParNew 另一区别:自适应调节策略

3、高吞吐量

(1)高效率地利用 CPU 时间,尽快完成程序的运算任务

(3)主要适合在后台运算,而不需要太多交互的任务

(2)常见在服务器环境中使用,如:执行批量处理、订单处理、工资支付、科学计算

4、在 JDK 1.6 时,提供用于执行老年代垃圾收集的 Parallel Old 收集器,代替老年代的 Serial Old 收集器

(1)Parallel Old 收集器采用标记-压缩算法、并行回收、Stop-The-World 机制

(2)在程序吞吐量优先的应用场景中,Parallel 收集器、Parallel Old 收集器的组合,在 Server 模式下的内存回收性能较高,在 Java 8 中,默认是此垃圾收集器

5、-XX:+UseParallelGC:手动指定年轻代使用 Parallel 并行收集器执行内存回收任务

6、-XX:+UseParallelOldGC:手动指定老年代使用并行收集器

(1)5、6 分别适用于新生代、老年代

(2)JDK 8 默认开启

(3)开启一个,另一个也会开启(互相激活)

7、-XX:ParallelGCThreads

(1)设置年轻代并行收集器的线程数

(2)一般与 CPU 核心数相等,以避免过多的线程数影响垃圾收集性能

8、-XX:MaxGCPauseMillis

(1)设置垃圾收集器最大停顿时间(即 STW 的时间),单位为毫秒

(2)为了尽可能地把停顿时间控制在 MaxGCPauseMills 以内,收集器在工作时会调整 Java 堆大小或者其他一些参数

(3)对于用户来讲,停顿时间越短体验越好,但是在服务器端,注重高并发,整体的吞吐量,所以服务器端适合 Parallel 进行控制

(4)该参数使用需谨慎

9、-XX:GCTimeRatio

(1)设置 N,垃圾收集时间占总时间的比例 = 1 / (N+1)

(2)用于衡量吞吐量的大小

(3)取值范围(0, 100),默认值为 99,即垃圾回收时间不超过 1%

(4)与 -XX:MaxGCPauseMillis 有一定矛盾性,暂停时间越长,Radio 参数越容易超过设定的比例

10、-XX:+UseAdaptivesizePolicy

(1)设置 Parallel Scavenge 收集器具有自适应调节策略

(2)这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数,自动调整在堆大小、吞吐量、停顿时间之间的平衡点

(3)在手动调优比较困难的场景,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills),虚拟机自动完成调优工作

 

Serial Old 收集器

1、Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用标记-压缩算法、Stop the World 机制

(2)Serial Old 是运行在 Client 模式下,默认的老年代的垃圾回收器

(3)Serial Old 在 Server 模式下,主要两个用途:与新生代的 Parallel Scavenge 配合使用;作为老年代 CMS 收集器的后备垃圾收集方案

2、这个收集器的主要意义是供客户端模式下的 HotSpot 虚拟机使用

3、在服务端模式下

(1)在 JDK 5 以及之前的版本中,与 Parallel Scavenge 收集器搭配使用,Parallel Scavenge 收集器架构中本身有 PS MarkSweep 收集器来进行老年代收集,并非直接调用 Serial Old 收集器,但是这个 PS MarkSweep 收集器与 Serial Old 的实现几乎是一样的

(2)作为 CMS 收集器发生失败时的后备预案,在并发收集发生 Concurrent Mode Failure 时使用

 

 

Parallel Old 回收器

1、Parallel Old 是 Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,基于标记-压缩算法实现

2、JDK 6 时开始提供

(1)在此之前,新生代的 Parallel Scavenge 收集器只能与 Serial Old(PS MarkSweep)收集器组合,因为 CMS 无法与它配合工作

(2)Serial Old 收集器在服务端的应用性能,使得 Parallel Scavenge 收集器未必能在整体上获得吞吐量最大化的效果

(3)由于单线程的老年代收集中无法充分利用服务器多处理器的并行处理能力,在老年代内存空间很大而且硬件规格比较高级的运行环境中,Parallel Scavenge + Serial Old 的总吞吐量甚至不一定比 ParNew + CMS 组合优秀

3、在注重吞吐量或处理器资源较为稀缺的场合,都可以优先考虑 Parallel Scavenge + Parallel Old 这个组合

 

CMS 回收器

1、低延迟

2、在 JDK 1.5 时,Hotspot 推出 CMS(Concurrent-Mark-Sweep)收集器

(1)HotSpot 虚拟机中第一款并发收集器,第一次实现使垃圾收集线程与用户线程同时工作

(2)尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验

(3)采用标记-清除算法、Stop-the-World 机制、并发回收

(4)作为老年代的收集器,无法与 JDK 1.4 中的新生代收集器 Parallel Scavenge 配合工作,所以在 JDK 1.5 中使用 CMS 来收集老年代时,新生代只能选择 ParNew 或 Serial 收集器中的一个

(5)在 G1 出现之前,CMS 使用广泛

3、过程

(1)初始标记(Initial-Mark)阶段:程序中所有的工作线程都因为 Stop-the-World 机制而出现短暂暂停,只标记出 GC Roots 直接关联到对象,一旦标记完成就会恢复之前被暂停的所有应用线程,由于直接关联对象比较小,所以这里的速度非常快

(2)并发标记(Concurrent-Mark)阶段:从 GC Roots 直接关联对象开始,遍历整个对象图,这个过程耗时较长,但不需要停顿用户线程,可以与垃圾收集线程一起并发运行

(3)重新标记(Remark)阶段:为了修正并发标记期间,因为用户程序继续运作,而导致标记产生变动的、某一部分对象的标记记录,这个阶段仍是 STW,停顿时间通常比初始标记阶段稍长,但远比并发标记阶段的时间短

(4)并发清除(Concurrent-Sweep)阶段:清理在(3)中判断已死亡的对象,释放内存空间,由于不需要移动存活对象,所以这个阶段可以与用户线程同时并发

4、CMS 收集器采用并发回收(非独占式)

(1)但在其初始标记、再次标记中,仍然需要执行 Stop-the-World 机制暂停程序中的工作线程,不过暂停时间并不会太长,可以说明目前所有垃圾收集器都做不到完全不需要 Stop-The-World,只能尽量缩短暂停时间

(2)因为最耗费时间的并发标记、并发清除都不需要暂停工作,所以整体回收是低延迟的

5、因为在垃圾收集阶段,用户线程没有中断

(1)所以在 CMS 回收过程中,应该确保应用程序用户线程,有足够的内存可用

(2)因此,CMS 收集器不能等到老年代几乎完全被填满,再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在 CMS 工作过程中,依然有足够的空间支持应用程序运行

(3)若 CMS 运行期间预留的内存无法满足程序需要,就会出现一次 Concurrent Mode Failure 失败,这时虚拟机将启动后备预案:临时启用 Serial Old 收集器,重新进行老年代的垃圾收集,但停顿时间很长

6、CMS 收集器采用标记清除算法

(1)每次执行完内存回收后,被执行内存回收的无用对象,所占用的内存空间,极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片

(2)则 CMS 在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配

7、不使用标记-压缩,代替标记-清除

(1)原因:当并发清除时,用 Compact 整理内存,用户线程无法使用内存

(2)要保证用户线程能继续执行,前提是不占用其运行资源,标记-压缩更适合 Stop The World 场景

8、优点

(1)并发收集

(2)低延迟

9、缺点

(1)产生内存碎片,导致并发清除后,用户线程可用的空间不足,在无法分配大对象的情况下,不得不提前触发 Full GC

(2)对 CPU 资源非常敏感,在并发阶段,它虽然不会导致用户停顿,但会因为占用一部分线程,而导致应用程序变慢,总吞吐量会降低

(3)无法处理浮动垃圾,可能出现 Concurrent Mode Failure 失败,而导致另一次 Full GC 产生;在并发标记阶段,因为程序的工作线程、垃圾收集线程是同时运行或交叉运行,在并发标记阶段如果产生新的垃圾对象,CMS 将无法对这些垃圾对象进行标记,导致新产生的垃圾对象没有被及时回收,只能在下一次执行 GC 时,释放这些之前未被回收的内存空间

10、-XX:+UseConcMarkSweepGC

(1)手动指定使用 CMS 收集器执行内存回收任务

(2)开启该参数后,自动打开 -XX:+UseParNewGC,即:ParNew(新生代使用)+ CMS(老年代使用)+ Serial Old(CMS 并发失败的补救措施)

(3)因为 CMS 为标记-清除算法,可能产生较多内存碎片,分配对象时,内存空间不足,CMS 退化为 Serial Old,串行 + 标记-整理算法,STW 时间较长

11、-XX:CMSInitiatingOccupanyFraction

(1)设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收

(2)JDK 5 及以前版本的默认值为 68,即当老年代的空间使用率达到 68% 时,执行一次 CMS 回收

(3)JDK 6 及以上版本默认值为 92

(4)如果内存增长缓慢,则可以设置一个稍大的值,大的阀值可以有效降低 CMS 的触发频率,减少老年代回收的次数,可以较为明显地改善应用程序性能

(5)如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器

(6)通过该选项便可以有效降低 Full GC 执行次数

12、-XX:+UseCMSCompactAtFullCollection

(1)开启功能:在执行完 Full GC 后,对内存空间进行压缩整理,以此避免内存碎片的产生

(2)由于内存压缩整理过程无法并发执行,使得停顿时间变得更长

13、-XX:CMSFullGCsBeforeCompaction

(1)设置在执行多少次 Full GC 后,对内存空间进行压缩整理

14、-XX:ParallelcMSThreads

(1)设置 CMS 线程数量

(2)CMS 默认启动的线程数 = (ParallelGCThreads + 3) / 4,ParallelGCThreads 是年轻代并行收集器的线程数

(3)当 CPU 资源比较紧张时,受到 CMS 收集器线程的影响,应用程序的性能在垃圾回收阶段会大幅下降

 

Serial GC、Parallel GC、Concurrent Mark Sweep GC

1、Serial GC:最小化地使用内存和并行开销

2、Parallel GC:最大化应用程序的吞吐量

3、CMS GC:最小化 GC 的中断或停顿时间

 

G1 回收器

1、区域化分代式

2、Garbage First(G1)

(1)在 Java 7 update4 之后引入

(2)目标:在延迟可控的情况下,获得尽可能高的吞吐量

(3)并行回收器,把堆内存分割为物理上不连续的区域(Region)

(4)使用不同的 Region 来表示 Eden、幸存者 0 区,幸存者 1 区,老年代等

(5)G1 有计划地避免在整个 Java 堆中进行全区域的垃圾收集,跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小、回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region

(6)垃圾优先(Garbage First):侧重在回收垃圾最大量的区间(Region)

(7)面向服务端应用的垃圾收集器,主要针对配备多核 CPU 及大容量内存的机器,以极高概率满足 GC 停顿时间的同时,还兼具高吞吐量的性能特征

(8)在 JDK1.7 版本正式启用,移除 Experimenta1 标识,是 JDK9 以后的默认垃圾回收器,取代 CMS 回收器以及 Parallel + Parallel Old 组合

(9)CMS 已经在 JDK9 中被标记为废弃(deprecated),在 JDK 8 中不是默认的垃圾回收器,需要使用 -XX:+UseG1GC 启用 G1

3、并发、并行

(1)并行性:G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力此时用户线程 STW

(2)并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况

4、分代收集

(1)仍区分年轻代、老年代,年轻代,依然有 Eden 区和 Survivor 区

(2)但从堆的结构上看,它不要求整个 Eden 区、年轻代、老年代都是连续的,不固定大小、数量,将堆空间分为若干个区域(Region)

(3)Region 中包含逻辑上的年轻代和老年代,但同时兼顾年轻代、老年代

5、空间整合

(1)G1 将内存划分为 region,以 region 作为基本单位回收内存

(2)Region 之间是复制算法,但整体上实际可看作标记-压缩算法,两种算法都可以避免内存碎片,有利于程序长时间运行,分配大对象时,不会因为无法找到连续内存空间,而提前触发下一次 GC,尤其是当 Java 堆非常大的时候,G1 的优势更加明显

6、可预测的停顿时间模型(即:软实时 soft real-time)

(1)让使用者明确指定,在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒

(2)因为分区,G1 可以只选取部分区域进行内存回收,缩小回收的范围,因此可以较好地控制全局停顿情况的发生

(3)G1 跟踪各个 Region 中的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region,保证 G1 收集器在有限的时间内,可以获取尽可能高的收集效率

(4)相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多

7、缺点

(1)相较于 CMS,G1 还不具备全方位、压倒性优势,比如在用户程序运行过程中,G1 垃圾收集产生的内存占用(Footprint),程序运行时的额外执行负载(Overload)都要比 CMS 要高

(2)从经验上来说,在小内存应用上 CMS 的表现大概率会优于 G1,而 G1 在大内存应用上则发挥其优势,平衡点在 6GB - 8GB 之间

8、参数设置

(1)-XX:+UseG1GC:手动指定使用 G1 垃圾收集器执行内存回收任务

(2)-XX:G1HeapRegionSize:设置每个 Region 的大小,值是 2 的幂,范围在 1MB - 32MB 之间,目标是根据最小的 Java 堆大小划分出约 2048 个区域,默认是堆内存的 1 / 2000

(3)-XX:MaxGCPauseMillis:设置期望达到的最大 GC 停顿时间指标(JVM 尽力实现,但不保证达到),默认值是 200ms

(4)-XX:+ParallelGCThread:设置 STW 工作线程数的值,最多设置为 8(以上 Parallel 回收器的线程计算公式,当 CPU_Count > 8 时,ParallelGCThreads 也会大于 8)

(5)-XX:ConcGCThreads:设置并发标记的线程数,将 n 设置为并行垃圾回收线程数(ParallelGCThreads)的 1 / 4 左右

(6)-XX:InitiatingHeapOccupancyPercent 设置触发并发 GC 周期的 Java 堆占用率阈值。超过此值,就触发 GC。默认值是 45

9、G1 收集器的常见操作(调优)步骤

(1)第一步:开启 G1 垃圾收集器

(2)第二步:设置堆的最大内存

(3)第三步:设置最大的停顿时间

(4)G1 中提供三种垃圾回收模式:Young GC、Mixed GC、Full GC,在不同的条件下被触发

10、应用场景

(1)面向服务端应用,针对具有大内存、多处理器的机器

(2)为需要低 GC 延迟,并具有大堆的应用程序提供解决方案

11、以下情况时,使用 G1 可能比 CMS 更好

(1)超过 50% 的 Java 堆被活动数据占用

(2)对象分配频率,或年代提升频率变化大

(3)GC 停顿时间过长(长于 0.5s - 1s)

(4)HotSpot 垃圾收集器中,除 G1 外,其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作,而 G1 GC 可以采用应用线程承担后台运行的 GC 工作,即当 JVM 的 GC 线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程

12、分区 Region:化整为零

(1)使用 G1 收集器时,它将整个 Java 堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32MB 之间,且为 2 的 N 次幂

(2)可以通过 -XX:G1HeapRegionSize 设定

(3)所有的 Region 大小相同,且在 JVM 生命周期内不会被改变

(4)虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离,它们都是一部分 Region(不需要连续)的集合,通过 Region 的动态分配方式实现逻辑上的连续

(5)一个 region 可能属于 Eden、Survivor 或 Old / Tenured 内存区域,但是一个 region 只可能属于一个角色

(7)G1 垃圾收集器增加一种新的内存区域:Humongous 内存区域,主要用于存储大对象,如果超过 1.5 个 region,就放到 Humongous

(8)设置 Humongous 原因:对于堆中的对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响

(9)如果一个 Humongous 区装不下一个大对象,则 G1 会寻找连续的 H 区来存储,为了能找到连续的 Humongous 区,有时不得不启动 Full GC,G1 大多数行为都把 H 区作为老年代的一部分来看待

(9)每个 Region 都是通过指针碰撞来分配空间

13、回收过程

(1)年轻代 GC(Young GC)

(2)老年代并发标记过程(Concurrent Marking)

(3)混合回收(Mixed GC)

(4)如果需要,单线程、独占式、高强度 Full GC 仍存在,它针对 GC 的评估失败提供一种失败保护机制,即强力回收

image-20200713224113996

(5)应用程序分配内存,当年轻代的 Eden 区用尽时,开始年轻代回收过程

(6)G1 的年轻代收集阶段,是一个并行、独占式收集器

(7)在年轻代回收期,G1 GC 暂停所有应用程序线程,启动多线程执行年轻代回收

(8)然后从年轻代区间移动存活对象,到 Survivor 区间或老年区间,有可能涉及两个区间

(9)当堆内存使用达到一定值(默认 45%)时,开始老年代并发标记过程

(10)标记完成马上开始混合回收过程,对于一个混合回收期,G1 GC 从老年区间移动存活对象到空闲区间,这些空闲区间成为老年代的一部分

(11)和年轻代不同,老年代的 G1 回收器和其他 GC 不同,G1 的老年代回收器不需要整个老年代被回收,一次只需要扫描 / 回收一小部分老年代的 Region 即可,同时,这个老年代 Region 是和年轻代一起被回收

14、卡表

(1)Card Table

(2)若 Region 中某个老年代对象,引用新生代对象,则为脏卡

15、Remembered Set(记忆集)

(1)一个对象被不同区域引用的问题:一个 Region 不可能是孤立的,一个 Region 中的对象可能被其他任意 Region 中对象引用,判断对象存活时,需要扫描整个 Java 堆才能保证准确,会降低 MinorGC 的效率

(2)无论 G1 还是其他分代收集器,JVM 都是使用 Remembered Set 来避免全局扫描

(3)每个 Region 都有一个对应的 Remembered Set

(4)每次 Reference 类型数据写操作时,都会产生一个 Post-Write Barrier(写屏障)暂时中断操作,每次引用变更时,都进行此异步操作,标记为脏卡

(5)然后检查将要写入的引用指向的对象,是否和该 Reference 类型数据在不同的 Region(其他收集器:检查老年代对象是否引用新生代对象)

(6)如果不同,通过 Card Table 把相关引用信息,记录到引用指向对象的所在 Region,对应的 Remembered Set 中

(7)当进行垃圾收集时,在 GC 根节点的枚举范围加入 Remembered Set,保证不进行全局扫描,也不会有遗漏

16、G1 回收过程一:年轻代 GC

(1)JVM 启动时,G1 先准备好 Eden 区,程序在运行过程中,不断创建对象到 Eden 区,当 Eden 空间耗尽时,G1 会启动一次年轻代垃圾回收过程

(2)年轻代垃圾回收只会回收 Eden 区和 Survivor 区

(3)首先 G1 停止应用程序的执行(Stop-The-World),G1 创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代 Eden 区和 Survivor 区所有的内存分段

(4)第一阶段:扫描根,根是指 static 变量指向的对象,正在执行的方法调用链条上的局部变量等,根引用连同 Remembered Set 记录的外部引用作为扫描存活对象的入口

(5)第二阶段:更新 Remembered Set,处理 Dirty Card Queue 中的 Card,更新 Remembered Set,此阶段完成后,Remembered Set 可以准确反映老年代对所在的内存分段中对象的引用

(6)第三阶段:处理 Remembered Set,识别被老年代对象指向 Eden 中的对象,这些被指向的 Eden 中的对象被认为是存活的对象

(7)第四阶段:复制对象,遍历对象树,Eden 区内存段中存活的对象,被复制到 Survivor 区中空的内存分段(to 区),Survivor 区内存段中存活的对象(from 区),如果年龄未达阈值,年龄会加 1,达到阀值会被会被复制到 Old 区中空的内存分段,如果 Survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间

(8)第五阶段:处理引用,处理 Soft,Weak,Phantom,Final,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作,而目标内存中的对象都是连续存储的,没有碎片,复制过程可以达到内存整理的效果,减少碎片

(9)Dirty Card Queue(脏卡队列):对于应用程序的引用赋值语句 Object.field = Object,JVM 会在之前、之后执行特殊的操作,以在 Dirty Card Queue 中入队一个保存对象引用信息的 Card,在年轻代回收时,GC 处理 Dirty Card Queue 中所有 Card,以更新 Remembered Set,保证 Remembered Set 实时准确的反映引用关系

(10)不在引用赋值语句处直接更新 Remembered Set,是为了性能需要,处理 Remembered Set 需要线程同步,开销会很大,使用队列性能更好

17、G1 回收过程二:Young GC + 并发标记过程

(1)在 Young GC 时会进行 GC Root 的初始标记,老年代占用堆空间比例达到阈值时,进行并发标记,由 -XX:InitiatingHeapOccupancyPercent=percent(默认 45%)决定

(2)初始标记阶段:标记从根节点直接可达的对象,该阶段 STW,并且会触发一次年轻代 GC

(3)根区域扫描(Root Region Scanning):G1 GC 扫描 Survivor 区直接可达的老年代区域对象,并标记被引用的对象,这一过程必须在 Young GC 之前完成

(4)并发标记(Concurrent Marking):在整个堆中进行并发标记(和应用程序并发执行),此过程可能被 Young GC 中断,若发现区域对象中的所有对象都是垃圾,则该区域会被立即回收,同时,会计算每个区域的对象活性(区域中存活对象的比例);进行 Pre-Write Barrier(写屏障),只要对象引用更改,则该对象进入 satb_mark_queue

(5)再次标记(Remark):由于应用程序持续进行,需要修正上一次的标记结果,检查并处理 satb_mark_queue,此阶段 STW,G1 采用比 CMS 更快的初始快照算法:Snapshot-At-The-Beginning(SATB)

(6)独占清理(Cleanup):计算各个区域的存活对象和 GC 回收比例,并进行排序,识别可以混合回收的区域,为下阶段做铺垫,此阶段 STW,并不会实际上去做垃圾的收集

(7)并发清理阶段:识别并清理完全空闲的区域

18、G1 回收过程三:混合回收

(1)当越来越多的对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,该算法并不是一个 Old GC,除了回收整个 Young Region,还会回收一部分 Old Region

(2)注意:是回收一部分老年代,而不是全部老年代,可以选择 Old Region 进行收集,从而控制垃圾回收的耗时,同时,Mixed GC 并不是 Full GC

(3)并发标记结束以后,老年代中回收绝对为垃圾的内存分段,计算出部分为垃圾的内存分段

(4)默认情况下,老年代的内存分段,会被分 8 次被回收(通过 -XX:G1MixedGCCountTarget 设置),但混合回收并不一定要进行 8 次

(5)混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden 区内存分段,Survivor 区内存分段

(6)混合回收的算法同样使用复制算法,只是包括回收老年代的内存分段,

(7)因为老年代中的内存分段默认分 8 次回收,G1 会优先回收垃圾多的内存分段,垃圾占内存分段比例越高的,越会被先回收

(8)过程:最终标记(Remark、STW) -> 拷贝存活(Evacuation、STW)

(9)-XX:G1MixedGCLiveThresholdPercent:决定内存分段是否被回收,默认为 65%,垃圾占内存分段比例要达到 65% 才会被回收,如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间

(10)-XX:G1HeapWastePercent:默认值为 10%,允许整个堆内存中有 10% 空间被浪费,如果发现可以回收的垃圾占堆内存的比例低于 10%,则不再进行混合回收,因为 GC 花费较多时间,但回收内存却很少

(11)-XX:MaxGCPauseMillis=ms:最大暂停时间,默认 200ms

19、G1 回收可选的过程四:Full GC

(1)G1 目的:避免 Full GC 出现

(2)如果上述方式不能正常工作,G1 会停止应用程序的执行(Stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长

(3)导致 G1 Full GC 两个原因:回收阶段(Evacuation)时,没有足够的 to-space 来存放晋升的对象;并发处理过程完成之前空间耗尽

20、回收阶段(Evacuation)不是与用户程序一起并发执行,而是并行回收

(1)考虑到 G1 只回收一部分 Region,停顿时间是用户可控制的,所以并不迫切去实现,选择把该特性放到低延迟垃圾收集器(ZGC)中

(2)考虑到 G1 不是仅面向低延迟,停顿用户线程能够最大幅度提高垃圾收集效率,为了保证吞吐量,所以才选择完全暂停用户线程的实现方案

21、优化 G1 回收器

(1)年轻代大小:避免使用 -Xmn 或 -XX:NewRatio 等相关选项显式设置年轻代大小;固定年轻代的大小会覆盖暂停时间目标

(2)G1 GC 的吞吐量目标:90% 应用程序时间 + 10% 垃圾回收时间,评估 G1 GC 吞吐量时,暂停时间目标不要太严苛,目标太过严苛表示可以承受更多的垃圾回收开销,而会直接影响吞吐量

 

截止 JDK 1.8,共有 7 款不同的垃圾收集器

垃圾收集器 分类 作用位置 使用算法 特点 适用场景
Serial 串行运行 作用于新生代 复制算法 响应速度优先 适用于单 CPU 环境下的 Client 模式
ParNew 并行运行 作用于新生代 复制算法 响应速度优先 多 CPU 环境 Server 模式下与 CMS 配合使用
Parallel Scavenge 并行运行 作用于新生代 复制算法 吞吐量优先 适用于后台运算而不需要太多交互的场景
Serial Old 串行运行 作用于老年代 标记-压缩算法 响应速度优先 适用于单 CPU 环境下的 Client 模式
Parallel Old 并行运行 作用于老年代 标记-压缩算法 吞吐量优先 适用于后台运算而不需要太多交互的场景
CMS 并发运行 作用于老年代 标记-清除算法 响应速度优先 适用于互联网或 B/S 业务
G1 并发、并行运行 作用于新生代、老年代 标记-压缩算法、复制算法 响应速度优先 面向服务端应用

 

选择垃圾收集器

1、优先调整堆的大小让 JVM 自适应完成

2、如果内存小于 100M,使用串行收集器

3、如果是单核、单机程序,并且没有停顿时间的要求,使用串行收集器

4、如果是多 CPU、需要高吞吐量、允许停顿时间超过 1 秒,选择并行,或由 JVM 选择

5、如果是多 CPU、追求低停顿时间,需快速响应,使用并发收集器

6、官方推荐 G1,性能高,目前普遍使用 G1

 

GC 日志分析

1、-XX:+PrintGC:输出 GC 日志

2、-verbose:gc:打开 GC 日志,只会显示总的 GC 堆的变化

3、-XX:+PrintGCDetails:输出 GC 详细日志

4、-XX:+PrintGCTimestamps:输出 GC 时间戳(以基准时间的形式)

5、-XX:+PrintGCDatestamps:输出 GC 时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)

6、-XX:+PrintHeapAtGC:在进行 GC  前后打印出堆的信息

7、-Xloggc:(文件路径):将日志的输出到指定文件

8、常用的日志分析工具:GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat 等

 

Open JDK12 的 Shenandoash GC

1、低停顿时间 GC

2、Oracle JDK 12 中不支持 Shenandoah

3、缺点:高运行负担下,吞吐量下降

4、优点:低延迟时间

 

ZGC

1、在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下,都可以把垃圾收集的停颇时间,限制在 10ms以内的低延迟

2、《深入理解 Java 虚拟机》定义:ZGC 收集器是一款基于 Region 内存布局的,(暂时)不设分代的,使用读屏障、染色指针、内存多重映射等技术,实现可并发的标记-压缩算法的,以低延迟为首要目标的一款垃圾收集器

3、工作过程可以分为 4 个阶段

(1)并发标记

(2)并发预备重分配

(3)并发重分配

(4)并发重映射

4、ZGC 几乎在所有地方并发执行,除了初始标记的是 STW,所以停顿时间几乎就耗费在初始标记上,这部分的实际时间是非常少的

(1)将在服务端、大内存、低延迟应用的首选垃圾收集器

(2)JEP 364:ZGC 应用在 macOS 上

(3)JEP 365:ZGC 应用在 Windows 上

(4)JDK14 之前,ZGC 仅 Linux 才支持

5、开启 ZGC

-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

 

其他垃圾回收器

1、AliGC:阿里巴巴 JVM 团队基于 G1 算法,面向大堆(LargeHeap)应用场景

2、低延迟 GC:Zing

posted @   半条咸鱼  阅读(417)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· DeepSeek “源神”启动!「GitHub 热点速览」
· 上周热点回顾(2.17-2.23)
点击右上角即可分享
微信分享提示