JVM垃圾回收器详解:不同的复制算法比较及对程序员的启迪

简介: 前面提到整个JVM中只有串行回收按照Cheney的设计实现新生代回收,其他的垃圾回收器在新生代回收时都对Cheney的复制算法进行了增强。其中最大的改变就是不使用宽度优先,而是使用深度优先的处理方式。其中Moon在1984年提出了一种近似深度优先遍历的处理方式,称为层次遍历,使用层次遍历大概可以将GC效果提升6%。

不同的复制算法比较及对程序员的启迪

前面提到整个JVM中只有串行回收按照Cheney的设计实现新生代回收,其他的垃圾回收器在新生代回收时都对Cheney的复制算法进行了增强。

其中最大的改变就是不使用宽度优先,而是使用深度优先的处理方式。其中Moon在1984年提出了一种近似深度优先遍历的处理方式,称为层次遍历,使用层次遍历大概可以将GC效果提升6%。

研究发现,宽度优先导致的性能问题在于数据局部性,这会导致数据访问缓存命中率下降。

那为什么宽度优先的复制算法会导致垃圾回收后应用运行时存在数据局部性问题呢?根本原因在于应用中对象的访问模型。测试结果表明,大多数应用在访问对象时同时访问父对象和子对象的概率更高,而同时访问两个兄弟对象的概率更低。也就说应用中对象的访问从统计角度更多的是按照深度优先进行,而不是按照宽度优先进行的。

这样的结果导致使用宽度优先复制算法在对象重排以后和应用中对象的访问模型并不一致,更容易导致缓存不命中,从而导致性能下降。

从这个角度出发,如果决定使用串行垃圾回收时,在开发应用的时候对象的访问尽量遵循宽度优先的访问方式;如果决定使用其他垃圾回收器,在开发应用的时候对象尽量采用深度优先的访问方式。这样的做法就能保证应用中对象的访问方式在垃圾回收执行完成之后和对象的组织方式一致,从而取得更好的运行效果。

最后需要指出的是,采用深度优先遍历的实现通常需要一个辅助空间栈(Stack)。但使用辅助栈会带来额外的问题,那就是栈空间应该多大?过大会导致空间浪费,太小则导致标记过程栈溢出。所以需要一种合理的设计,既不浪费空间,又能在栈溢出时正确处理。那么JVM是如何解决这个问题的呢?读者可以先思考一下这个问题。

虽然JVM中并没有采用Moon算法实现分代的复制,但是在另一款开源的JVM产品OpenJ9中,分代回收的复制算法优先使用的就是并行的Moon算法。

这里简单介绍一下Moon的算法,由于Moon算法是Cheney算法的优化,因此我们先回顾一下Cheney的算法思想,再来看看Moon是如何优化的。

Cheney算法可以简单总结为:在往目标空间复制对象的时候,额外引入了两个指针,分别为Scan和Free,Scan和Free直接在内存空间中模拟了队列,队列中存放的是待处理的对象,从Scan开始遍历直到Scan和Free重合,对象就全部处理完成。如图3-38所示,分配空间用浅蓝色表示,To空间中使用3种颜色描述对象处理的状态。

网络异常,图片无法展示
|

图3-38 Cheney算法示意图

Moon算法的改进主要包含如下几个要点:

1)将目标空间划分为更细粒度的块,如图3-39中的A、B、C、D、E。

2)在标记/复制的时候,总是从最后一个尚未填满的块开始,如图3-39中的块D会被先处理,所以使用了一个PrimaryScan表示扫描先从这里开始。

当PrimaryScan和Free之间的对象标记/复制完成之后(注意,这里仍然采用宽度优先遍历的方式进行处理),如果块D的剩余空间不能满足对象的填充,那么块E会被使用。

3)当块D被填满之后,再按照SecondaryScan指向的位置开始再次进行标记/复制。当再次出现了未填满的块时,则按照上一步中的方法继续优先处理。

从这些描述可以看出,Moon算法最大的一个缺点是有些对象可能需要扫描2次才能完成。例如图中的D填充满了之后,从块B开始扫描,然后再扫描块C(实际上块C的对象在第一次遇到未填满的时候已经进行了一次扫描)。算法的示意图如图3-39所示。

网络异常,图片无法展示
|

图3-39 Moon算法示意图

使用Moon算法的效果如何?这里采用论文中的示例来演示一下,如图3-40所示。

  • 宽度优先算法,得到对象的存储顺序为O1、O2、……、O14、O15。
  • 深度优先复制算法,得到对象的存储顺序为O1、O2、O4、O8、O9、
  • O5、O10、O11、O3、O6、O12、O13、O7、O14、O15。

使用Moon修正的算法,假设每个块只能保存3个对象,得到对象的存储顺序为O1、O2、O3、O4、O8、O9、O5、O10、O11、O6、O12、O13、O7、O14、O15。

网络异常,图片无法展示
|

图3-40 Moon算法示例

结论是,深度优先得到的对象存储序列和Moon的修正算法仅仅只有O3的位置有所不同,所以Moon算法也被称为近似的深度优先复制算法。同时也要注意,块大小影响最终的结果。在本例中块大小设置为保存3个对象,最后的结果和深度优先复制算法类似。但是当块大小设置得更大一些,结果就可能与深度优先差别比较大了。

例如块的大小设置为保持5个对象,此时得到对象的存储顺序为:O1、O2、O3、O4、O5、O6、O12、O13、O7、O14、O15、O8、

O9、O10、O11。可以看到,此时对象的存储顺序与宽度优先遍历更为接近(假设以对象在相同位置为标准)。再比如设置块大小为保存15个对象,此时Moon就完全退化为宽度优先复制算法了。从这里可以看出这个算法的关键是为块设置一个合适的大小。

另外,Moon是一个串行的算法,Siegwart在2006年将Moon算法修改成并行算法。OpenJ9中层次遍历就是基于这篇论文实现的。由于并行算法涉及任务切分、任务均衡等细节,本章主要关注串行算法的实现,因此不对OpenJ9中的实现展开介绍。

在这里,我们谈论了宽度优先遍历、层次遍历和深度优先遍历。研究表明,层次遍历的效果比宽度优先遍历好。那么层次遍历和深度优先遍历的效果相比如何?

2018年,OpenJ9社区发起了一个针对层次遍历的优化,为OpenJ9中增加一款基于深度优先遍历的并行实现。

这里仅仅介绍一下优化改进后的效果,其作者针对3种应用分别使用层次遍历和深度优先遍历进行了测试,从测试结果来看在吞吐量和停顿时间上,基于深度优先遍历的回收算法都有明显的优势。测试选择的3种应用是:交易型应用(Transactional)、数据处理型应用(Data Compilation)和数据库应用(Database Processing)。测试中使用OpenJ9的gencon垃圾回收器,该垃圾回收器的新生代既支持宽度优先遍历又支持层次遍历,默认使用层次遍历。作者在测试中指明了测试使用的OS、内存大小等信息,对测试过程感兴趣的读者可以阅读原文。深度优先和层次优先遍历的性能测试对比结果如表3-4所示。

网络异常,图片无法展示
|

表3-4 深度优先和层次遍历优先性能测试结果

针对3种应用,GC的吞吐量分别提升22.671%、20.864%和2.805%;GC的停顿时间分别减少18.01%、16.074%和3.113%。另外,原文中还给出了测试套跑分的提升,但比较遗憾的是作者并未给出测试套的更多信息,所以本书并未列出。另外,需要注意的是,该优化最终并未合入OpenJ9的主线版本(可能与OpenJ9的技术路线有关)。

最后做一个简单的总结:从目前的论文研究和测试结果来看,使用深度优先遍历的回收算法效果最好,层次遍历次之,宽度优先遍历最差;深度优先遍历在实现时有额外的内存空间开销,而层次遍历和宽度优先遍历没有。

对于程序员来说,应了解不同产品中采用的算法以及算法本身的使用场景,在开发应用时结合选择的回收算法,使用深度优先的方式或者宽度优先的方式来访问对象,可以获得额外的收益。

本文给大家讲解的内容是JVM垃圾回收器详解:串行回收,不同的复制算法比较及对程序员的启迪

相关文章
|
3月前
|
存储 算法 Oracle
极致八股文之JVM垃圾回收器G1&ZGC详解
本文作者分享了一些垃圾回收器的执行过程,希望给大家参考。
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
62 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
存储 监控 算法
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程 ?
尼恩提示: G1垃圾回收 原理非常重要, 是面试的重点, 大家一定要好好掌握
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程  ?
|
1月前
|
算法 Java
JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
本文详细介绍了JVM中的GC算法,包括年轻代的复制算法和老年代的标记-整理算法。复制算法适用于年轻代,因其高效且能避免内存碎片;标记-整理算法则用于老年代,虽然效率较低,但能有效解决内存碎片问题。文章还解释了这两种算法的具体过程及其优缺点,并简要提及了其他GC算法。
 JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
|
1月前
|
算法 Java
谈谈HotSpot JVM 中的不同垃圾回收器
【10月更文挑战第5天】理解 HotSpot JVM 中的不同垃圾回收器(如 CMS、G1 和 ZGC)的区别,需要深入了解它们的设计原理、工作方式和应用场景。以下是对这三个垃圾回收器的简要概述以及一个示例 Java 程序,虽然示例程序本身不能直接展示垃圾回收器的内部机制,但可以帮助观察不同垃圾回收器的行为。
25 1
|
1月前
|
存储 算法 Java
【JVM】垃圾释放方式:标记-清除、复制算法、标记-整理、分代回收
【JVM】垃圾释放方式:标记-清除、复制算法、标记-整理、分代回收
49 2
|
1月前
|
负载均衡 监控 算法
每个程序员都应该知道的 6 种负载均衡算法
每个程序员都应该知道的 6 种负载均衡算法
97 2
|
30天前
|
算法 JavaScript 前端开发
垃圾回收算法的原理
【10月更文挑战第13天】垃圾回收算法的原理
22 0
|
2月前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
112 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
|
2月前
|
算法 程序员 Python
程序员必看!Python复杂度分析全攻略,让你的算法设计既快又省内存!
在编程领域,Python以简洁的语法和强大的库支持成为众多程序员的首选语言。然而,性能优化仍是挑战。本文将带你深入了解Python算法的复杂度分析,从时间与空间复杂度入手,分享四大最佳实践:选择合适算法、优化实现、利用Python特性减少空间消耗及定期评估调整,助你写出高效且节省内存的代码,轻松应对各种编程挑战。
41 1