问题
代际屏障为什么会影响JVM的性能?
基础知识
大部分的垃圾回收期都是分代的,或者是分区域的。如果任何垃圾收集器想要收集托管堆的部分而不触及整个堆,那么它必须了解哪些引用指向该收集部分。否则,它无法可靠地判断该收集部分中什么是可访问的,因此必须保守地假设一切都是可访问的…… 这使得它处于没有任何东西可以被视为垃圾的境地。其次,如果它移动了该收集部分中的任何对象,它希望了解要更新哪些位置 — 这主要涉及在处理收集部分时不会被访问的“外部”指针。
那垃圾回收器是如何解决这个难题的呢?其实只要将堆分成多个部分就可以,将堆分成几部分的最简单(也是最有效的)方法是按年龄隔离对象,即引入代数。这里的关键思想是弱代假说,即“新对象会早逝”。利用弱代假说的实际情况是,在标记收集器时,性能取决于幸存对象的数量。这意味着,我们可以有一个年轻代,其中所有东西都基本死了,我们可以快速甚至更频繁地处理它,而老一代则被放在一边。
但是,在单独收集年轻代时,可能需要注意从老代到年轻代的引用。老年代通常与整个堆一起收集,年轻代对老年代的引用可以从跟踪中省略。年轻代对年轻代和老年代到老年代的引用也是如此,因为它们的引用将在同一次收集中被访问。
以 OpenJDK 的 Parallel GC 为例,它借助卡表记录老年代对年轻代的引用:卡表是横跨老年代的粗略位图。存储时,需要翻转该卡表中的一位。该位意味着老年代“卡片下方”的部分可能具有指向年轻代的指针,并且在收集年轻代时需要对其进行扫描。要使所有这些工作正常,引用存储必须通过写屏障进行增强,即执行卡表管理的小代码片段。
实验
用例源码
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 3, jvmArgsAppend = {
"-Xms4g", "-Xmx4g", "-Xmn2g"})
@Threads(Threads.MAX)
public class HashWalkBench {
@Param(