详解 G1 垃圾收集器

第一章 概述

G1(Garbage First)垃圾收集器是当今垃圾回收技术最前沿的成果之一。早在 JDK7 就已加入 JVM 的收集器大家庭中,成为 HotSpot 重点发展的垃圾回收技术。同优秀的 CMS 垃圾回收器一样,G1 也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方也推荐使用 G1 来代替选择 CMS。G1 最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至 CMS 的众多缺陷。

第二章 JVM GC收集器的回顾与比较

从 JDK3(1.3)开始,HotSpot 团队一直努力朝着高效收集、减少停顿(STW: Stop The World)的方向努力,也贡献了从串行到 CMS 乃至最新的 G1 在内的一系列优秀的垃圾收集器。上图展示了 JDK 的垃圾回收大家庭,以及相互之间的组合关系,下面就几种典型的组合应用进行简单的介绍。

串行收集器

串行收集器组合 Serial + Serial Old

开启选项:-XX:+SerialGC

串行收集器是最基本、发展时间最长、久经考验的垃圾收集器,也是 client 模式下的默认收集器配置。

串行收集器采用单线程 stop-the-world 的方式进行收集。当内存不足时,串行 GC 设置停顿标识,待所有线程都进入安全点(Safepoint)时,应用线程暂停,串行 GC 开始工作,采用单线程方式回收空间并整理内存。单线程也意味着复杂度更低、占用内存更少,但同时也意味着不能有效利用多核优势。事实上,串行收集器特别适合堆内存不高、单核甚至双核 CPU 的场合。

并行收集器

并行收集器组合 Parallel Scavenge + Parallel Old

开启选项:-XX:+UseParallelGC-XX:+UseParallelOldGC(可互相激活)

并行收集器是以关注吞吐量为目标的垃圾收集器,也是 server 模式下的默认收集器配置,对吞吐量的关注主要体现在年轻代 Parallel Scavenge 收集器上。

并行收集器与串行收集器工作模式相似,都是 stop-the-world 方式,只是暂停时并行地进行垃圾收集。年轻代采用复制算法,老年代采用标记-整理,在回收的同时还会对内存进行压缩。关注吞吐量主要指年轻代的 Parallel Scavenge 收集器,通过两个目标参数 -XX:MaxGCPauseMills-XX:GCTimeRatio,调整新生代空间大小,来降低 GC 触发的频率。并行收集器适合对吞吐量要求远远高于延迟要求的场景,并且在满足最差延时的情况下,并行收集器将提供最佳的吞吐量。

并发标记清除收集器

并发标记清除收集器组合 ParNew + CMS + Serial Old

开启选项:-XX:+UseConcMarkSweepGC

并发标记清除(CMS)是以关注延迟为目标、十分优秀的垃圾回收算法,开启后,年轻代使用 STW 式的并行收集,老年代回收采用 CMS 进行垃圾回收,对延迟的关注也主要体现在老年代 CMS 上。

年轻代 ParNew 与并行收集器类似,而老年代 CMS 每个收集周期都要经历:初始标记、并发标记、重新标记、并发清除。其中,初始标记以 STW 的方式标记所有的根对象;并发标记则同应用线程一起并行,标记出根对象的可达路径;在进行垃圾回收前,CMS 再以一个 STW 进行重新标记,标记那些由 mutator 线程(指引起数据变化的线程,即应用线程)修改而可能错过的可达对象;最后得到的不可达对象将在并发清除阶段进行回收。值得注意的是,初始标记和重新标记都已优化为多线程执行。CMS 非常适合堆内存大、CPU 核数多的服务器端应用,也是 G1 出现之前大型应用的首选收集器。

但是 CMS 并不完美,它有以下缺点:

  • 由于并发进行,CMS 在收集与应用线程会同时会增加对堆内存的占用,也就是说,CMS 必须要在老年代堆内存用尽之前完成垃圾回收,否则 CMS 回收失败时,将触发担保机制,串行老年代收集器将会以 STW 的方式进行一次 GC,从而造成较大停顿时间;
  • 标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS 也提供了参数 -XX:CMSFullGCsBeForeCompaction(默认0,即每次都进行内存整理)来指定多少次 CMS 收集之后,进行一次压缩 的Full GC。

Garbage First

开启选项:-XX:+UseG1GC

之前介绍的几组垃圾收集器组合,都有几个共同点:

  • 年轻代、老年代是独立且连续的内存块;
  • 年轻代收集使用单 eden、双 survivor 进行复制算法;
  • 老年代收集必须扫描整个老年代区域;
  • 都是以尽可能少而块地执行 GC 为设计原则。

G1 垃圾收集器也是以关注延迟为目标、服务器端应用的垃圾收集器,被 HotSpot 团队寄予取代 CMS 的使命,也是一个非常具有调优潜力的垃圾收集器。虽然 G1 也有类似 CMS 的收集动作:初始标记、并发标记、重新标记、清除、转移回收,并且也以一个串行收集器做担保机制,但单纯地以类似前三种的过程描述显得并不是很妥当。事实上,G1 收集与以上三组收集器有很大不同:

  • 1、G1 的设计原则是"首先收集尽可能多的垃圾(Garbage First)"。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。同时G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大;
  • 2、G1 采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此 G1 天然就是一种压缩方案(局部压缩);
  • 3、G1 虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的 survivor(to space)堆做复制准备。G1 只有逻辑上的分代概念,或者说每个分区都可能随 G1 的运行在不同代之间前后切换;
  • 4、G1 的收集都是 STW 的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。

第三章 G1的内存模型

分区概念

分区 Region

G1 采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1 并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数 -XX:G1HeapRegionSize=n 可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。

卡片 Card

在每个分区内部又被分成了若干个大小为512 Byte 卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见 RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。

堆 Heap

G1 同样可以通过 -Xms/-Xmx 来指定堆空间大小。当发生年轻代收集或混合收集时,通过计算 GC 与应用的耗费时间比,自动调整堆空间大小。如果 GC 频率太高,则通过增加堆尺寸,来减少 GC 频率,相应地 GC 占用的时间也随之降低;目标参数 -XX:GCTimeRatio 即为 GC 与应用的耗费时间比,G1 默认为 9,而 CMS 默认为 99,因为 CMS 的设计原则是耗费在 GC 上的时间尽可能的少。另外,当空间不足,如对象空间分配或转移失败时,G1 会首先尝试增加堆空间,如果扩容失败,则发起担保的 Full GC。Full GC 后,堆尺寸计算结果也会调整堆空间。

This chapter requires login to view full content. You are viewing a preview.

Login to View Full Content

Course Curriculum

3

框架与 I/O:Spring、Netty 与 Web 容器

理解 Spring Boot 自动装配、AOP 与事务原理,掌握 Netty Reactor 模型及 Tomcat 连接处理机制,构建高内聚、易扩展的应用服务层。
4

高性能中间件:消息、缓存与存储

熟练运用 MySQL 索引/事务、Redis 缓存策略、Kafka/RocketMQ 消息可靠性,以及 ZooKeeper 分布式协调,搭建稳定、解耦的分布式数据底座。
6

云原生:容器化、可观测性与工程效能

通过 Docker/K8s 实现弹性部署,集成 Metrics/Logs/Traces 构建可观测体系,推动 DevOps 与自动化,让架构在云上持续交付与进化。