目录

JVM垃圾收集器基准报告 – Ionuț Baloșin

各项基准:

BurstHeapMemoryAllocator基准

ConstantHeapMemoryOccupancyBenchmark

HeapMemoryBandwidthAllocatorBenchmark

ReadWriteBarriers基准

WriteBarriersLoopingOverArrayBenchmark

ReadBarriersLoopingOverArrayBenchmark

ReadBarriersChainOfClassesBenchmark

NMTMeasurementsMain

结论


JVM垃圾收集器基准报告 – Ionuț Baloșin

本文是我根据  google 翻译自垃圾收集器基准报告,翻译的不清晰请勿怪。

希望读者甚于借鉴。

本文使用一组不同的模式描述了一系列Java虚拟机(JVM)垃圾收集器(GC)微基准及其结果。对于当前问题,我包括了AdoptOpenJDK 64位服务器VM版本13(内部版本13 + 33)中的所有垃圾收集器:

  • 串行GC
  • Parallel / ParallelOld GC (启动Java 7u4 ParallelGC和ParallelOld GC基本上是同一收集器)
  • 并发标记扫描CMS GC (目前不建议使用,它将根据JEP 363在Java 14版本中删除)
  • 垃圾优先G1 GC
  • Shenandoah GC
  • ZGC (目前处于实验阶段)
  • Epsilon GC  (目前处于实验状态)

我故意选择了AdoptOpenJDK,因为并非所有的OpenJDK构建都包括Shenandoah GC。

当前所有GC基准测试都集中在以下指标上:

  1. (a)在相对较小和较大对象的不同分配率下,GC对象回收的效率;(b)具有或不具有恒定的堆预分配部分。
  2. 遍历和/或更新堆数据结构时尝试读写屏障的影响,并尝试避免在基准方法中进行任何显式分配,除非它是由基础生态系统引起的。
  3. 内部GC本机结构的占用空间。

配置

  • 所有基准都是用Java编写的,并使用JMH v1.22
  • 基准测试源代码不是公开的,但我详细介绍了它们所依赖的优化模式。
  • 每个基准测试使用5x10s的热身迭代,5x10s的测量迭代,3个JVM分支,并且是单线程的。
  • 所有针对高对象分配率的基准测试,都将初始堆大小设置为与最大堆大小相同的值,而且还会预先触摸页面以避免调整大小和内存提交打commit(例如-Xms4g -Xmx4g -XX:+ AlwaysPreTouch)。
  • 所有测试均在具有以下配置的计算机上启动:
    • CPU:Intel i7-8550U Kaby Lake R
    • 内存:32GB DDR4 2400 MHz
    • 作业系统:Ubuntu 19.04 / 5.0.0-37-generic
  • 为了消除动态频率缩放的影响,我禁用了intel_pstate驱动程序,并将CPU调节器设置为performance。
  • 请记住,当前的基准测试可能会受到其他因素的影响,例如即时编译器优化,底层库(例如JMH),CPU缓存和分支预测效果,内存分配器子系统等。
  • 所有基准测试结果(包括吞吐量,gc.alloc.rate.norm,gc.count,gc.time等)都合并在我的GitHub帐户的专用HTML报告中。为了获得更好的图表质量,我建议您打开HTML报告,因为当前帖子仅包含打印屏幕(通常用于吞吐量测量)。您还可以在GitHub的同一存储库下找到原始基准测试结果(即JSON测试结果)。

一点理论

在进一步介绍之前,我想简要地介绍一些理论,以更好地了解即将到来的基准测试。

什么是垃圾回收机制(GC)? 它是一种自动内存管理形式。垃圾收集器尝试回收由程序不再使用的对象所占用的垃圾或内存。值得一提的是,垃圾回收器除了回收(对于不再可访问的对象)外,还进行对象的分配。

分代 GC意味着将数据划分为多个分配区域,这些区域根据对象的使用期限(即,幸存的GC迭代次数)保持分开。虽然有些收集器是单代的,但其他收集器则使用两个堆代:

(1)年轻代(划分为Eden 代和两个Survivor代)

(2)老生代。

单代GC:

  • Shenandoah  GC
  • ZGC

两代GC:

  • 串行GC
  • Parallel/ParallelOld GC
  • CMS GC
  • G1 GC

读/写屏障(Read/write barriers)是一种在对对象进行读/写时执行一些额外的内存管理代码的机制。即使没有真正的GC发生,这些障碍通常也会影响应用程序的性能(只是读/写)。让我们考虑以下伪代码:

object.field = some_other_object // writeobject = some_other_object.field // read

使用读/写障碍的概念,从概念上讲,它可能类似于:

write_barrier(object).field = some_other_objectobject = read_barrier(some_other_object).field

关于上述AdoptOpenJDK收集器,使用的读/写障碍如下:

  • 写屏障

    • 一个写屏障(用于跟踪从老生代到年轻代的引用,例如,卡片表),用于:

      • 串行GC
      • Parallel/ParallelOld GC
      • CMS GC
    • 一个写屏障(在程序运行时并发标记的情况下,例如,Snapshot-At-The-Beginning (SATB)用于:
      • Shenandoah GC
    • 两个写屏障:(a)首先,在并发标记(例如SATB)的情况下是PreWrite屏障,(b)其次,再是PostWrite屏障,不仅要跟踪从老生代到年轻代的引用,还要跟踪任何跨区域引用(例如,记忆集):
      • G1 GC
  • 读屏障
    • Shenandoah  GC

      • 在OpenJDK版本<= 12的情况下通过引用访问对象的字段(即,每次访问都引用Brooks指针)时
      • 如果OpenJDK版本> = 13,则从堆中加载引用(即,加载引用屏障(LRB))时
    • ZGC,当从堆中加载引用时
  • 没有障碍
    • Epsilon GC完全不使用任何屏障,并且可以用作所有其他收集器的基准

超出范围

  • 有关每个垃圾收集器如何工作的更多详细信息。互联网上有很多这样的材料(例如演示,书籍,博客等),其呈现方式比我可能写的要好。
  • 任何其他JVM实现和GC类型(至少目前是这样)。
  • 除4GB以外的任何其他JVM Heap大小。
  • 除了报告的JHM时序外,还有任何关于为何基准X优于或劣于基准Y的详细说明。但是,我可以将资源提供给可能对重现该场景并进行进一步分析感兴趣的任何HotSpot工程师。
  • 真实应用程序上的任何端到端宏基准测试。这可能是最有代表性的,但是,当前的重点是微基准测试。

各项基准:

BurstHeapMemoryAllocator基准

该基准测试 创建了许多临时对象,在ArrayList中保持对它们的强引用,直到它填充了一定比例的Heap占用率,然后释放了它们(即调用blackhole.consume()),因此它们都突然有资格使用垃圾收集器。

void allocate(int sizeInBytes, int numberOfObjects) {for (int iter = 0; iter < 4; iter++) {List<byte[]> junk = new ArrayList<>(numberOfObjects);for (int j = 0; j < numberOfObjects; j++) {junk.add(new byte[sizeInBytes]);}blackhole.consume(junk);}
}

sizeInBytes为:

  • _4_KB
  • _4_MB

自动计算numberOfObjects以仅消耗60%的可用堆内存

结论

  • ZGC和Shenandoah GC的性能明显优于其他所有收集器。
  • G1 GC的吞吐量比ZGC和Shenandoah GC差,但是在_4_MB对象(即,根据G1术语为巨大对象)的情况下,其性能明显优于CMS GC,ParallelOld GC和串行GC。

(banq注:适合突然而来的尖锋访问)

ConstantHeapMemoryOccupancyBenchmark

此基准最初(在设置过程中)分配了许多对象,作为堆的预分配部分,并对其保持强烈引用,直到它填满一定百分比的堆占用率(例如70%)。预先分配的对象由大量的复合类组成(例如,类C1->类C2->…->类C32)。这可能会影响GC根遍历(例如,在“并行”标记阶段),因为遍历对象图时指针间接定向(即参考处理)的程度不可忽略。

然后,在基准测试方法中,分配了临时对象(大小为8 MB)并立即释放,因此它们很快就可以使用垃圾收集器。由于这些对象被认为是大对象,因此它们通常遵循缓慢的路径分配,直接驻留在“老生代”中(对于代收集者而言),从而增加了使用完整GC (FullGC) 的可能性。

void test(Blackhole blackhole) {blackhole.consume(new byte[_8_MB]);
}

结论

  • CMS GC和G1 GC的性能明显优于其他所有GC。
  • 也许令人惊讶的是,ZGC和Shenandoah GC的吞吐量最差。

(banq注:程序有缓存机制,启动时预先warm了内存,加载了一些热点数据在内存中,或者使用类似Redis原理的常驻内存机制)

对于ZGC和Shenandoah GC,这可以通过在每个周期标记整个堆以回收垃圾的成本来解释。在单代GC的情况下,收集(简单的)垃圾仍然需要标记整个可到达的预分配对象。通过使用-XX:ConcGCThreads = <n>增加并发GC线程数,可以部分缓解此问题。否则,世代GC的工作就是在收集(简单的)垃圾时跳过其余的堆。

ParallelOld GC和Serial GC陷入了由大量分配(例如8 MB大小的字节数组)引起的过早的完全GC综合症。但是,过早的完整GC也可能是由非大量分配引起的。

在G1 GC的情况下,巨大的分配具有特定的处理方式(即,它们被分配到专用区域中),并且在撤离暂停期间(在每个Young GC上)收集起始Java 8u40。

HeapMemoryBandwidthAllocatorBenchmark

此基准测试分配大小不同块的分配率。与以前的基准测试(例如,ConstantHeapMemoryOccupancyBenchmark)相比,它只是分配临时对象并立即释放它们,而没有保留任何预分配的对象。

byte[] allocate() {return new byte[sizeInBytes];
}

sizeInBytes为:

  • _4_KB
  • _4_MB

结论

  • 对于较大的对象(例如_4_MB),G1 GC的响应时间最差(比所有其他Collector慢5倍),而ParallelOld GC似乎是最高效的。
  • 对于相对较小的对象(例如_4_KB),结果几乎相同,但对Shenandoah GC的支持略好一些,但没有相关的区别。

对读写屏障测试点击标题见原文

  • 每个GC都有不同的内存占用空间来保存其内部GC本机结构。尽管这可能会受到堆大小的影响(即,增加/减小堆大小也可能会增加/减少GC本机结构的占用空间),但很显然,所有GC都需要额外的本机内存来进行堆管理。
  • 除Epsilon GC外,最小的内存属于串行GC,其次是CMS GC,ZGC,Shenandoah GC,ParallelOld GC和G1 GC。

ReadWriteBarriers基准

测试读/写屏障的开销,同时遍历整数数组并在它们之间的每个值之间交换值(即array [i] <-> array [j])。整数数组在设置过程中初始化,因此在基准测试方法中,几乎不存在分配数。

void test() {int lSize = size;int mask = lSize - 1;for (int i = 0; i < lSize; i++) {Integer aux = array[i];array[i] = array[(i + index) & mask];array[(i + index) & mask] = aux;}index++;
}

结论

  • Epsilon GC由于不使用任何读/写屏障,因此可提供最佳吞吐量。包括它只是为了为其他收集器提供基准。
  • Shenandoah GC似乎比任何其他收集器都要好(当然,除了Epsilon GC)。
  • G1 GC提供最差的吞吐量,比其他吞吐量慢约10倍至14倍。在这种情况下,PostWrite障碍(即,跨G1区域的“记忆集”管理)可能是其背后的原因。

WriteBarriersLoopingOverArrayBenchmark

在遍历整数数组的元素并更新其中的每个元素时,测试写屏障的开销。基准测试方法中的分配数量保持为零。

void test(Integer lRefInteger) {int lSize = size;for (int i = 0; i < lSize; i++) {array[i] = lRefInteger;}
}

结论

  • Epsilon GC由于不使用任何读/写屏障,因此可提供最佳吞吐量。包括它只是为了为其他收集器提供基准。
  • ZGC的性能似乎比任何其他收集器都要好(当然,除了Epsilon GC外)。
  • G1 GC提供了最差的吞吐量,比其余的要慢大约10到20倍。它最有可能与先前的基准测试具有相同的根本原因(即PostWrite障碍开销)。

由于缺少压缩的OOP,因此在ZGC的支持下产生了细微的差别。对于其他收集器(包括Shenandoah GC),访问压缩OOP需支付少量费用。由于ZGC不支持压缩OOP,因此在这里具有优势。

ReadBarriersLoopingOverArrayBenchmark

在遍历整数数组的元素并读取其中的每个元素时,测试读取障碍的开销。取消装箱效果(即int <-Integer之间的转换)也是此基准的副作用。

注意:遍历数组有利于可以提升屏障而不需要真正考虑屏障本身成本的算法。

int test() {int lSize = size;int sum = 0;for (int i = 0; i < lSize; i++) {sum += array[i];}return sum;
}

结论

  • ZGC提供最低的吞吐量。显然,可以用两件事来解释:(1)无法提升读取屏障检查;(2)缺少压缩的OOP,这现在反过来了(与以前的基准相比;例如WriteBarriersLoopingOverArrayBenchmark)。由于整数数组足够大,因此适合放入CPU缓存的机会较少。
  • 在所有其他情况下,结果都非常相似,这从本质上强调了 Shenandoah GC通过将LRB提升到循环之外,在优化LRB方面做得更好(当然,压缩OOP的存在很重要)。提醒一下,所有其他收集器(例如串行GC,ParallelOld GC,CMS GC,G1 GC,Epsilon GC)均不使用任何读取屏障。

尽管在以后的版本中可能会修复读取屏障提升,但是在ZGC的情况下,压缩OOP的缺失是设计的约束。

ReadBarriersChainOfClassesBenchmark

在遍历一大堆预分配的复合类实例(例如,类H1->类H2->…->类H32)进行迭代时,测试读取屏障的开销,并返回由其最内部保留的字段属性。

int test() {return  h1.h2.h3.h4.h5.h6.h7.h8.h9.h10.h11.h12.h13.h14.h15.h16.h17.h18.h19.h20.h21.h22.h23.h24.h25.h26.h27.h28.h29.h30.h31.h32.aValue;
}// where:
class H1 {H2 h2;H1(H2 h2) {this.h2 = h2;}
}// ...class H32 {int aValue;public H32(int aValue) {this.aValue = aValue;}
}

结论

  • Epsilon GC由于不使用任何读/写屏障,因此可提供最佳吞吐量。包括它只是为了为其他收集器提供基准。
  • 在所有其他情况下,吞吐量是相同的,没有明显的差异。

NMTMeasurementsMain

此方案运行一个简单的“ Hello World” Java程序,该程序在JVM生命周期结束时打印堆管理所需的内部GC本机结构的大小。这样的结构可能是:

  • 标记位图–标记可访问的Java对象所需
  • 标记堆栈– GC算法本身遍历对象图所需的标记
  • 记忆集–跟踪跨区域引用所必需(对于G1 GC)

该测试依赖于本机内存跟踪(NMT),该程序跟踪(即检测)内部JVM分配并报告GC本机结构的占用空间。

public class NMTMeasurementsMain {public static void main(String []args) {System.out.println("Hello World");}}

使用以下JVM参数模式,每次通过指定不同的GC类型多次启动“ Hello World” Java程序:

-XX:+ UnlockDiagnosticVMOptions -XX:+使用<Type> GC -XX:+ PrintNMTStatistics -XX:NativeMemoryTracking = summary -Xms4g -Xmx4g -XX:+ AlwaysPreTouch
  • 类型  为{Serial,ParallelOld,ConcMarkSweep,G1,Shenandoah,Z,Epsilon}
// Serial GC
(reserved=13_694KB, committed=13_694KB)
(malloc=34 KB)
(mmap: reserved=13_660KB, committed=13_660KB)// ParallelOld GC
(reserved=182_105KB, committed=182_105KB)
(malloc=28_861KB)
(mmap: reserved=153_244KB, committed=153_244KB)// CMS GC
(reserved=34_574KB, committed=34_574KB)
(malloc=19_514KB)
(mmap: reserved=15_060KB, committed=15_060KB)// G1 GC
(reserved=216_659KB, committed=216_659KB)
(malloc=27_711KB)
(mmap: reserved=188_948KB, committed=188_948KB)// Shenandoah GC
(reserved=136_082KB, committed=70_538KB)
(malloc=4_994KB)
(mmap: reserved=131_088KB, committed=65_544KB)// ZGC
(reserved=8_421_751KB, committed=65_911KB)
(malloc=375KB)
(mmap: reserved=8_421_376KB, committed=65_536KB)// Epsilon GC
(reserved=29KB, committed=29KB)
(malloc=29KB)

说明:

  • 已提交:已用除PROT_NONE以外的内容映射的地址范围
  • reserved:已为特定内存池预映射的总地址范围

结论

  • 每个GC都有不同的内存占用空间来保存其内部GC本机结构。尽管这可能会受到堆大小的影响(即,增加/减小堆大小也可能会增加/减少GC本机结构的占用空间),但很显然,所有GC都需要额外的本机内存来进行堆管理。
  • 除Epsilon GC外,最小的占用空间属于串行GC,其次是CMS GC,ZGC,Shenandoah GC,ParallelOld GC和G1 GC。

最终结论

请不要过于虔诚地接受此报告,因为它涵盖了所有可能的用例。此外,某些基准可能存在缺陷,而另一些基准可能需要付出更多的努力才能深入研究并试图理解这些数字背后的真正原因(超出范围)。即使这样,我认为它仍可以为您提供更广泛的了解,并证明没有一个垃圾收集器适合所有情况。双方各有利弊,各有千秋。

根据当前的基准设置和此特定设置很难提供一般性结论。不过,我将其总结为:

  • 对于G1 GC,“记忆集”的管理有很大的开销。
  • 当大量已分配实例(占堆大小的60%)有资格进行回收时,ZGC和Shenandoah GC似乎非常有效。
  • 当堆Heap不包含许多其他可以在GC迭代之间存活的强可访问实例时,ParallelOld GC可以很好地回收短期分配的对象。
  • CMS GC和G1 GC似乎提供了更好的吞吐量,同时当大量Heap堆(大约70%)不断被占用时,回收临时分配的大对象(例如8_MB),因此在GC迭代之间可以生存的实例非常强大。

即使有一些通用的GC特性,您也可以大致了解哪种特性更适合您的应用程序:

  • 串行GC占用空间最小,并且可能是参考实现(即最简单的GC算法)。
  • ParallelOld GC尝试针对高吞吐量进行优化。
  • G1 GC努力在吞吐量和暂停时间之间取得平衡。
  • ZGC努力争取尽可能短的暂停时间(例如,最大10毫秒),并且旨在从较小的堆大小到较大的堆大小(即,从数百MB到很多TB)更好地扩展。
  • Shenandoah GC的目标是低暂停时间,不再与堆大小成正比。

(banq注:吞吐量与暂停是一对矛盾,ParallelOld GC注重高吞吐量,而Shenandoah GC是注重低暂停,其他是在这两个极端之间平滑)

参考文献
  • https://ionutbalosin.com/2019/12/jvm-garbage-collectors-benchmarks-report-19-12/
  • http://htmlpreview.github.io/?https://github.com/ionutbalosin/jvm-performance-benchmarks-reports/blob/master/19_12_report_openjdk-13/jmh_visualizer_gc/index.html
  • Shenandoah GC Wiki
  • JEP 333: ZGC – A Scalable Low-Latency Garbage Collector
  • ZGC – Low Latency GC for OpenJDK – Stefan Karlsson and Per Liden
  • Understanding Low Latency JVM GCs – Jean-Philippe Bempel
  • Write Barriers in G1 GC – by Monica Beckwith
  • Intergenerational Barriers – by Aleksey Shipilëv

JVM垃圾收集器基准报告 – Ionuț Baloșin 如何选择适合你的垃圾回收器相关推荐

  1. 直通BAT必考题系列:7种JVM垃圾收集器特点,优劣势、及使用场景

    直通BAT之JVM系列 直通BAT必考题系列:JVM的4种垃圾回收算法.垃圾回收机制与总结 直通BAT必考题系列:深入详解JVM内存模型与JVM参数详细配置 今天继续JVM的垃圾回收器详解,如果说垃圾 ...

  2. JVM垃圾收集器(三)

    JVM垃圾收集器(三) 垃圾回收(GC)线程与应用线程保持相对独立,当系统需要执行垃圾回收任务时,先停止工作线程,然后命令GC线程工作,以串行模式工作的收集器,称为Serial Collector,即 ...

  3. 7种 JVM 垃圾收集器特点、优劣势及使用场景(多图)

    点击上方"IT牧场",选择"设为星标"技术干货每日送达! 一.常见垃圾收集器 现在常见的垃圾收集器有如下几种: 新生代收集器: Serial ParNew Pa ...

  4. JVM垃圾收集器与内存分配策略学习总结

    方法区: 1.线程共享 2.储存类信息,常量,静态变量,编译器编译后的代码 3.非堆(别名)用于区分Java堆 4.不需要连续的内存 5.可以固定或可扩张 6.选择不实现垃圾回收//这个区域很少进行垃 ...

  5. JVM垃圾收集器——G1

    导航 引言 一.G1 介绍 1.1 适用场景 1.2 设计初衷 1.3 关注焦点 1.4 工作模式 1.5 堆的逻辑结构 1.6 主要收集目标 1.7 停顿预测模型 1.8 拷贝和压缩 1.9 与 C ...

  6. JVM优化系列-JVM垃圾收集器介绍

    导语   既然是串行顾名思义就是使用单线程的方式进行执行,每次执行回收的时候,串行回收器只有一个工作线程,这样对于并行能力较弱的计算机,串行回收器更多的独占线程专一执行的方面有着良好的实现,也就是说在 ...

  7. jvm垃圾收集器与内存分配策略

    2019独角兽企业重金招聘Python工程师标准>>> 垃圾收集器与内存分配策略: 以下参考周志明的<<深入理解jvm高级特性与最佳实践>>. 判断对象是否存 ...

  8. JVM 垃圾收集器(Garbage Collection)

    判断对象是否存活 在堆里边存放着java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,首先需要确定这些对象之中哪些还"存活"着,哪些已经"死去"(即不可 ...

  9. JVM:垃圾收集器与内存分配策略

    垃圾收集器与内存分配策略 1.对象已死吗 1).引用计数算法 引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1:当引用失效时,计数器值就减1:任何时刻计数器为0的对象就 ...

  10. JVM 垃圾收集器 学习笔记(《深入理解java虚拟机》之六 垃圾收集)

    目录 新生代收集器 Serial收集器 ParNew收集器 Parallel Scavenge收集器 老年代收集器 Serial Old收集器 Parallel Old收集器 CMS收集器 Remov ...

最新文章

  1. NLP-基础知识-001
  2. [JSOI2010] 满汉全席
  3. iOS之AVPlayerViewController的使用oc
  4. 鬼才项斌,用人工智能推动教育服务创新
  5. 14 WM配置-主数据-定义存储单位类型(Storage Unit Type)
  6. L323 英语有必要学语法吗
  7. windows10完全删除mysql_Windows 10系统下彻底删除卸载MySQL的方法教程
  8. 计算机无法读取exe文件,电脑打不开exe文件怎么解决
  9. CSS优先级、引入方式、Hack
  10. jd反编译java_java反编译工具jd
  11. adb命令查看手机上的APP包名和启动activity
  12. 温度补偿计算公式_基于温度压力补偿计算的燃气表计量方法与流程
  13. 斗鱼扩展--移除广告优化页面(五)
  14. Windows7,8,10 启用网络发现却无法保存设置-----解决办法
  15. Photoshop制作剔透魔法水晶球
  16. 【Python爬虫网站数据实战】Python爬虫 统计淘宝商品数据+数据可视化
  17. 直播连麦实现的三种方法
  18. “智数据,创未来”——2018中国存储与数据峰会在京盛大举行
  19. 微信公众号测试账号网址
  20. 大型企业局域网安全解决方案

热门文章

  1. arch 关闭独显_manjaro上安装独显驱动(双显卡切换)的正确方法
  2. ps端午节计算机辅助设计,PS教程:端午节的字体设计
  3. halcon模板匹配快速入门
  4. 陕西电力同业对标管理系统
  5. 第06章 数据挖掘综合应用
  6. ADS1115 应用指导
  7. Tensorflow2对GPU内存的分配策略
  8. VScode底部状态栏不见,手把手教学
  9. 研究生课程笔记:软件包在流行病学中的应用(二)——csurvey软件+抽样调查
  10. 魔王抓住了公主 等笑话