为什么使用Epsilon和G1观察到重复的内存分配比较慢?

kaa*_*aan 16 java garbage-collection

我很想知道使用G1和Epsilon在JDK 13中分配内存所花费的时间。我观察到的结果是出乎意料的,我对了解正在发生的事情感兴趣。最终,我希望了解如何使Epsilon的使用性能比G1高(或者,如果不可能,为什么)。

我写了一个小测试,反复地分配内存。根据命令行输入,它将:

  • 创建1,024个新的1 MB阵列,或者
  • 创建1,024个新的1 MB阵列,测量分配的时间,并打印出每个分配的经过时间。这不仅测量分配本身,而且还包括两次调用之间发生的任何其他事件所花费的时间System.nanoTime()-仍然,这似乎是一个有用的信号。

这是代码:

public static void main(String[] args) {
    if (args[0].equals("repeatedAllocations")) {
        repeatedAllocations();
    } else if (args[0].equals("repeatedAllocationsWithTimingAndOutput")) {
        repeatedAllocationsWithTimingAndOutput();
    }
}

private static void repeatedAllocations() {
    for (int i = 0; i < 1024; i++) {
        byte[] array = new byte[1048576]; // allocate new 1MB array
    }
}

private static void repeatedAllocationsWithTimingAndOutput() {
    for (int i = 0; i < 1024; i++) {
        long start = System.nanoTime();
        byte[] array = new byte[1048576]; // allocate new 1MB array
        long end = System.nanoTime();
        System.out.println((end - start));
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我正在使用的JDK的版本信息:

$ java -version
openjdk version "13-ea" 2019-09-17
OpenJDK Runtime Environment (build 13-ea+22)
OpenJDK 64-Bit Server VM (build 13-ea+22, mixed mode, sharing)
Run Code Online (Sandbox Code Playgroud)

这是我运行程序的不同方式:

  • 仅使用G1进行分配: $ time java -XX:+UseG1GC Scratch repeatedAllocations
  • 仅分配,Epsilon: $ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
  • 分配+时序+使用G1的输出: $ time java -XX:+UseG1GC Scratch repeatedAllocationsWithTimingAndOutput
  • 分配+时序+输出,Epsilon: time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocationsWithTimingAndOutput

以下是仅使用分配运行G1的一些时间安排:

$ time java -XX:+UseG1GC Scratch repeatedAllocations
real    0m0.280s
user    0m0.404s
sys     0m0.081s

$ time java -XX:+UseG1GC Scratch repeatedAllocations
real    0m0.293s
user    0m0.415s
sys     0m0.080s

$ time java -XX:+UseG1GC Scratch repeatedAllocations
real    0m0.295s
user    0m0.422s
sys     0m0.080s

$ time java -XX:+UseG1GC Scratch repeatedAllocations
real    0m0.296s
user    0m0.422s
sys     0m0.079s
Run Code Online (Sandbox Code Playgroud)

以下是仅使用分配运行Epsilon的一些时间安排:

$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
real    0m0.665s
user    0m0.314s
sys     0m0.373s

$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
real    0m0.652s
user    0m0.313s
sys     0m0.354s

$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
real    0m0.659s
user    0m0.314s
sys     0m0.362s

$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
real    0m0.665s
user    0m0.320s
sys     0m0.367s
Run Code Online (Sandbox Code Playgroud)

无论有没有定时+输出,G1都比Epsilon快。作为额外的度量,使用中的定时号repeatedAllocationsWithTimingAndOutput,当使用Epsilon时,平均分配时间更大。具体来说,一个本地运行显示G1GC平均每个分配227,218纳秒,而Epsilon平均521,217纳秒(我捕获了输出编号,粘贴到电子表格中,并对average每组编号使用该函数)。

我的期望是,Epsilon测试的速度会明显提高,但是实际上我看到的速度要慢2倍。G1的最大分配时间肯定更长,但只是间歇性的-大多数G1分配的速度明显慢于Epsilon,几乎慢了一个数量级。

这是repeatedAllocationsWithTimingAndOutput()使用G1和Epsilon 运行1,024次的曲线图。深绿色代表G1;深绿色代表G1。浅绿色表示Epsilon;Y轴是“每分配纳米数”;Y轴次方网格线每250,000纳秒。它表明Epsilon分配时间非常一致,每次大约300-400k nanos。这也表明,G1时间在大多数情况下明显更快,但也比Epsilon慢约10倍。我认为这归因于垃圾收集器的运行,这是理智且正常的,但似乎也否定了G1足够聪明以至于它不需要分配任何新内存的想法。

在此处输入图片说明

Ale*_*lev 29

我相信您会在首次访问时看到连接内存的成本。

在Epsilon的情况下,分配总是到达新内存,这意味着OS本身必须将物理页面连接到JVM进程。在G1情况下,会发生相同的事情,但是在第一个GC周期之后,它将在已连接的内存中分配对象。G1偶尔会遇到与GC暂停相关的延迟跳跃。

但是有操作系统的特点。至少在Linux上,当JVM(或实际上是任何其他进程)“保留”并“提交”内存时,实际上并未连接内存:也就是说,尚未为其分配物理页面。作为优化,Linux在第一次对该页面进行写访问时进行了连接。sys%顺便说一下,该OS活动将显示为,这就是为什么您会在时间上看到它。

当您优化内存占用时,例如在机器上运行的许多进程,(预)分配大量内存但很少使用它,这无疑是OS正确的选择。例如,这将发生-Xms4g -Xmx4g:OS会愉快地报告所有4G都已“提交”,但是直到JVM开始在那里写之前,什么都不会发生。

所有这些都是导致这个怪异技巧的开始:从JVM开始预触摸所有堆内存-XX:+AlwaysPreTouch(注意head,这是第一个示例):

$ java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -Xms4g -Xmx4g \
       Scratch repeatedAllocationsWithTimingAndOutput | head
491988
507983
495899
492679
485147

$ java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -XX:+AlwaysPreTouch -Xms4g -Xmx4g \
       Scratch repeatedAllocationsWithTimingAndOutput | head
45186
42242
42966
49323
42093
Run Code Online (Sandbox Code Playgroud)

在这里,开箱即用的运行确实使Epsilon看起来比G1差(请注意tail,这是最后一个示例):

$ java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -Xms4g -Xmx4g \
       Scratch repeatedAllocationsWithTimingAndOutput | tail
389255
386474
392593
387604
391383

$ java -XX:+UseG1GC -Xms4g -Xmx4g \
  Scratch repeatedAllocationsWithTimingAndOutput | tail
72150
74065
73582
73371
71889
Run Code Online (Sandbox Code Playgroud)

...但是一旦连接内存超出画面tail,这种情况就会改变(注意,这是最后的示例):

$ java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -XX:+AlwaysPreTouch -Xms4g -Xmx4g \
       Scratch repeatedAllocationsWithTimingAndOutput | tail
42636
44798
42065
44948
42297

$ java -XX:+UseG1GC -XX:+AlwaysPreTouch -Xms4g -Xmx4g \
        Scratch repeatedAllocationsWithTimingAndOutput | tail
52158
51490
45602
46724
43752
Run Code Online (Sandbox Code Playgroud)

G1也有所改进,因为它在每个周期后都会接触到一些新的内存。Epsilon速度更快,因为它要做的事更少。

总体而言,这就是为什么-XX:+AlwaysPreTouch对于可以接受前期启动成本和前期RSS足迹付款的低延迟/高吞吐量工作负载的推荐选项。

UPD:考虑一下,这是Epsilon UX错误,简单的特性应该向用户发出警告

  • 出色的细节和信息。也添加了此内容–从[Java HotSpot VM Options](https://www.oracle.com/technetwork/articles/java/vmoptions-jsp-140102.html)`-XX:+ AlwaysPreTouch`:_“ Pre-touch JVM初始化期间的Java堆。因此,堆的每个页面在初始化期间都按需置零,而不是在应用程序执行期间递增。 (3认同)

kaa*_*aan 4

@Holger 上面的评论解释了我在原始测试 \xe2\x80\x93 中缺少的部分,从操作系统获取新内存比在 JVM 中回收内存更昂贵。@the8472 的评论指出,应用程序代码没有保留对任何已分配数组的引用,因此测试没有测试我想要的内容。通过修改测试以保留对每个新阵列的引用,结果现在显示 Epsilon 的性能优于 G1。

\n\n

这是我在代码中所做的保留引用的操作。将其定义为成员变量:

\n\n
static ArrayList<byte[]> savedArrays = new ArrayList<>(1024);\n
Run Code Online (Sandbox Code Playgroud)\n\n

然后在每次分配后添加:

\n\n
savedArrays.add(array);\n
Run Code Online (Sandbox Code Playgroud)\n\n

Epsilon 分配与之前类似,符合预期:

\n\n
$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations\nreal    0m0.587s\nuser    0m0.312s\nsys     0m0.296s\n\n$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations\nreal    0m0.589s\nuser    0m0.313s\nsys     0m0.297s\n\n$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations\nreal    0m0.605s\nuser    0m0.316s\nsys     0m0.313s\n
Run Code Online (Sandbox Code Playgroud)\n\n

G1 时间现在比以前慢得多,也比 Epsilon 慢:

\n\n
$ time java -XX:+UseG1GC Scratch repeatedAllocations\nreal    0m0.884s\nuser    0m1.265s\nsys     0m0.538s\n\n$ time java -XX:+UseG1GC Scratch repeatedAllocations\nreal    0m0.884s\nuser    0m1.251s\nsys     0m0.533s\n\n$ time java -XX:+UseG1GC Scratch repeatedAllocations\nreal    0m0.864s\nuser    0m1.214s\nsys     0m0.528s\n
Run Code Online (Sandbox Code Playgroud)\n\n

使用 重新运行每次分配时间repeatedAllocationsWithTimingAndOutput(),平均值现在与 Epsilon 匹配,速度更快。

\n\n
average time (in nanos) for 1,024 consecutive 1MB array allocations\nEpsilon 491,665\nG1      883,981\n
Run Code Online (Sandbox Code Playgroud)\n