kaa*_*aan 16 java garbage-collection
我很想知道使用G1和Epsilon在JDK 13中分配内存所花费的时间。我观察到的结果是出乎意料的,我对了解正在发生的事情感兴趣。最终,我希望了解如何使Epsilon的使用性能比G1高(或者,如果不可能,为什么)。
我写了一个小测试,反复地分配内存。根据命令行输入,它将:
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)
这是我运行程序的不同方式:
$ time java -XX:+UseG1GC Scratch repeatedAllocations
$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
$ time java -XX:+UseG1GC Scratch repeatedAllocationsWithTimingAndOutput
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错误,简单的特性应该向用户发出警告。
@Holger 上面的评论解释了我在原始测试 \xe2\x80\x93 中缺少的部分,从操作系统获取新内存比在 JVM 中回收内存更昂贵。@the8472 的评论指出,应用程序代码没有保留对任何已分配数组的引用,因此测试没有测试我想要的内容。通过修改测试以保留对每个新阵列的引用,结果现在显示 Epsilon 的性能优于 G1。
\n\n这是我在代码中所做的保留引用的操作。将其定义为成员变量:
\n\nstatic ArrayList<byte[]> savedArrays = new ArrayList<>(1024);\n
Run Code Online (Sandbox Code Playgroud)\n\n然后在每次分配后添加:
\n\nsavedArrays.add(array);\n
Run Code Online (Sandbox Code Playgroud)\n\nEpsilon 分配与之前类似,符合预期:
\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\nG1 时间现在比以前慢得多,也比 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 匹配,速度更快。
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