俗说 GC 之 Heap 区内存模型的演进
Qunar技术沙龙
2021 年 3 月 17 日
编程语言JavaJVM
俗说 GC 之 Heap 区内存模型的演进
- 篇首
最近看到 GC 这个异常古老的话题又被抛了出来,OpenJDK11 中出现了大道至简、返璞归真的 Epsilon GC 垃圾回收器,ZGC,Shenandoah GC 作为在 G1 的基础之上的两个分别合入了 OpenJDK11 和 OpenJDK12 的项目,两个项目的代号分别为分别为 JEP333、JEP189,这两款目前正在发展中的垃圾回收器成为了与 CMS、G1 两款早期并发回收器进行性能对比的主角。然后我就突然找到了一个主题,那就是比较两款垃圾回收器的性能就是在比较什么?要比较的究竟是哪些性能?是否有一款垃圾回收器能够在性能维度的方方面面碾压另一款垃圾回收器?这个问题我心里有自己的一些看法。我也一直在思考,在 JVM 这方面,周志明老师的大作《深入理解 Java 虚拟机》第三版于 2019 年中完成,其中对 ZGC,ShenandoahGC 两个面向低延时的垃圾回收器已经做了详细的原理解析,是否还需要再写文章做一些说明呢。但是转念又想,对于 JVM 垃圾回收器这部分虽然有如此多的珠玉在前,但是如果我自己的观点哪怕只有一点点是有价值的,那么不分享出来让大家看到也是一种知识价值的浪费。所以几番犹豫后,还是决定把自己对于 JVM GC 的理解写下来分享给大家。
那么我们说的 JVM 垃圾回收器不可能的三角是个什么规模的事情呢?先看一张图,我们都知道,我们可以把 JDK、JRE、JVM 的关系这样来表示:
而我们本文需要讨论的内容仅仅是 Java HotSpot Client and Server VM 中的一部分:垃圾回收器
描述一个垃圾回收器最重要的指标是哪些呢?
按照周志明老师在《深入理解 Java 虚拟机》2019 年第三版中的描述:分别是——
a. Heap 区间的内存占用(堆外辅助内存空间大小)
b. 无 GC 情况下的吞吐量(读写屏障的影响)
c. 延迟停顿( STW 时长),这三个指标形成了一个类似于事务的 CAP 理论的不可能三角。
我们在形容垃圾回收器的时候所采用的所有维度说明,都是为了解决这三件事之间的矛盾,无外乎是选择谁、牺牲谁或者给谁多点、给谁少点的问题。
再看一张图,我们在讨论垃圾回收器的时候通常会讨论如下问题:
下面我们对这三组分类:内存模型分类、回收算法按执行过程分类和回收算法按算法理念分类,逐一做一个简单说明,本文限于篇幅,核心关注点是按照内存模型进行分类。
根据 java 虚拟机规范,java 虚拟机管理的内存将分为下面五大区域。
严格来说,下面的图不能够叫内存模型,应该叫内存的划分:
JVM 垃圾回收器主要管理的是这五大内存区域中的堆区间,我们所说的内存模型主要说的就是 JVM 堆区的内存模型。
对于 JVM 来说,通过我的总结,堆内存空间的类型大致分为四类:
A.分代模型 B.分代分区模型 C.分区模型 D.分层分区模型
援引我司冯志明老师对于内存模型的一段解释性说明,这段说明娓娓道来、简单明确,不援引出来分享给大家十分可惜:
内存模型(也叫内存一致性模型)这个概念是来自硬件的。是为了解决 CPU 缓存和内存一致性问题所构造出来的一个概念。
因为有读写两个操作,所以一致性场景有 4 个:LoadLoad,LoadStore,StoreLoad,StoreStore 。
每款 CPU 都有自己的内存模型。有的是强一致性模型,比如 x86,就只有 StoreLoad 场景下,会有一致性问题。有的是弱一致性模型,比如 ARMv7,就是 4 个场景都有一致性的问题。
一致性模型越弱,CPU 结构会越简单,性能也会越高。但是对于软件开发者的要求也越高。
Linux 提供了 3 种汇编原语,来应对不同的一致性场景,那就是:写屏障,读屏障和全屏障。就是为了针对不同 CPU 下,不同内存模型下,做好数据同步操作。
JVM 是软件,也是虚拟机(虚拟的 CPU),所以 JVM 也一定也会有自己的内存模型(JMM)。
这个内存模型还要涵盖所有 CPU 下的内存模型,所以 JMM 就更通用也更晦涩。
所以以前的 JMM 定义了 8 大操作(已经过时的概念)read,load,store,write 等等很晦涩的内容。
工作内存很容易被误解成内存中的一个部分,其实它指的是 CPU 缓存。
JMM 模型虽然分了 4 个场景,但是观察 x86 下的 JVM 源码,就会发现,只有 StoreLoad()里面有真正的屏障代码。其他三个屏障函数,都是空的。
总结来看,就是 JMM 是通用的内存模型,JVM 根据不同的 CPU,再遵从 CPU 的内存模型而执行代码。
- 分代模型
Serial 收集器、ParNew 收集器、Parallel Scavenge 收集器、Serial Old 收集器、Parallel Old 收集器、CMS 收集器。堆内存分为新生代和老年代,新生代和老年代是物理隔离的。
分代模型细致介绍:我们现在常说的分代回收模型——如下图所示,也叫做 David Ungar 堆内存模型:
该内存模型的设计者及垃圾回收算法的发明人是一位叫做 David Ungar 的美国工程师,关于分代回收最早的描述出现在 David Ungar 1984 年的论文《Generation Scavenging: A Non-Disruptive High Performance Storage Reclamation Algorithm》中。
为什么会有分代回收产生?主要原因是基于内存中的大量对象会在较短的时间内达到不可达。很多对象用过几次之后就没用了虽然是经验之谈,但是适用于大多数的情况。在周志明老师的书中有三个构成分代收集基础的假说,分别是:
1)弱分代假说:绝大多数对象都是朝生夕灭的。
2)强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
3)跨代引用假说:跨代引用相对于同代引用来说仅占极少数。
以这三个假说为前提,新生代 GC 只把新生成的对象当做对象,这样一来,通过减小对象范围,能够减少相同 Heap 区间内垃圾回收的时间消耗。老年代 GC 是针对较难变为垃圾的老年代对象执行的。执行老年代 GC 要比执行新生代 GC STW 间隔要更长但是不会长于不分代的情况下如果执行 fullgc 的长度。
假设在不分代的情况下与分代回收算法老年代通常采用的垃圾回收器,如标记清除算法相同,我们可以认为这种算法与不分代的标记清除算法相比 GC 吞吐效率提升 4 倍,最大 STW 间隔时间不变。
值得注意的是,这种 GC 吞吐效率提升是概率性的,如果恰好这个服务生成的对象都会生存很久,那么我们的 GC 吞吐效率反倒会降低,因为分代回收会在做 FGC 的基础之上,多做多次 YGC,即便不考虑 YGC 的因素,也会因为分代产生的写屏障而降低吞吐效率。
- 本文总结
首先通过一张表对本文所写的内容做一个小结:
如同本文开始说的,GC 可以从多个不同的角度去理解和分类,而任何一本描述 GC 的书,都要有其思路轨迹和分类脉络。没有任何一种分类可以说是完全科学的,本文先按照 Heap 内存模型这个角度对 GC 垃圾回收器做一个分类,本文对每个垃圾回收器的描述并不全面,只是从 Heap 区内存模型这一个角度来描述,原因也是后面的文字会从其他的角度来继续描述目前 Hotspot JVM 支持的各垃圾回收器。期待最终我把各段文字拼凑在一起,包括我在内的读者可以大致看到各垃圾回收器的全貌。
本文还强调的一点是,无论从 GC 垃圾回收器的任何一个角度来看垃圾回收器的特点,目的都是做到:
a. Heap 区间的内存占用(堆外辅助内存空间大小)
b. 无 GC 情况下的吞吐量(读写屏障的影响)
c. 延迟停顿(STW 时长) 这三件事情的再平衡
在硬件条件既定的情况下,不存在既要 Heap 区利用充分,也要程序吞吐量大,还要延迟停顿低这三件事情同时发生的情况,只是在对的硬件环境下,用对的 GC 垃圾回收器,达到更好的平衡,这点也是我自己不仅使用垃圾回收器,而且尽量挤时间去了解一些垃圾回收器原理的目的。
参考书目:
《C4: The Continuously Concurrent Compacting Collector 》Gil Tene ,Balaji Iyengar, Michael Wolf
《Shenandoah An open-source concurrent compacting garbage collector for OpenJDK 》Christine H. Flood ,Roman Kennke ,Andrew Dinn ,Andrew Haley, Roland Westrelin
《Generation Scavenging: A Non-Disruptive High Performance Storage Reclamation Algorithm》David Ungar
《Virtual Machines》 James Smith, Ravi Nair
《JEP 333: ZGC: A Scalable Low-Latency Garbage Collector》Per Liden, Stefan Karlsson
《JEP 189: Shenandoah: A Low-Pause-Time Garbage Collector》Christine H. Flood, Roman Kennke
《深入理解 Java 虚拟机》周志明
《垃圾回收的算法与实现》中村成洋
《ZGC 设计与实现》彭成寒