java为什么JVM在一次繁忙的旋转暂停后,会对同一代码块显示更多的延迟?
下面的代码明确地说明了问题,即:
The exact same block of code becomes slower after a busy spin pause.
请注意,我当然没有使用Thread.sleep
。还请注意,当我使用数学运算而不是IF
更改暂停时,没有导致热点/JIT取消优化的条件
- 有一个数学运算块,我想计时李>
- 首先,在开始测量之前,我将块暂停1纳秒。我做了两万次李>
- 然后我将暂停时间从1纳秒改为5秒,然后像往常一样继续测量延迟。我做了15次李>
- 然后我打印最后30个测量值,这样你可以看到15个测量值,暂停1纳秒,15个测量值,暂停5秒李>
如下图所示,差异很大,尤其是在暂停更改后的第一次测量中这是为什么强>
$ java -server -cp . JvmPauseLatency
Sat Apr 29 10:34:28 EDT 2017 => Please wait 75 seconds for the results...
Sat Apr 29 10:35:43 EDT 2017 => Calculation: 4.0042328611017236E11
Results:
215
214
215
214
215
214
217
215
216
214
216
213
215
214
215
2343 <----- FIRST MEASUREMENT AFTER PAUSE CHANGE
795
727
942
778
765
856
762
801
708
692
765
776
780
754
守则:
import java.util.Arrays;
import java.util.Date;
import java.util.Random;
public class JvmPauseLatency {
private static final int WARMUP = 20000;
private static final int EXTRA = 15;
private static final long PAUSE = 5 * 1000000000L; // in nanos
private final Random rand = new Random();
private int count;
private double calculation;
private final long[] results = new long[WARMUP + EXTRA];
private long interval = 1; // in nanos
private long busyPause(long pauseInNanos) {
final long start = System.nanoTime();
long until = Long.MAX_VALUE;
while(System.nanoTime() < until) {
until = start + pauseInNanos;
}
return until;
}
public void run() {
long testDuration = ((WARMUP * 1) + (EXTRA * PAUSE)) / 1000000000L;
System.out.println(new Date() +" => Please wait " + testDuration + " seconds for the results...");
while(count < results.length) {
double x = busyPause(interval);
long latency = System.nanoTime();
calculation += x / (rand.nextInt(5) + 1);
calculation -= calculation / (rand.nextInt(5) + 1);
calculation -= x / (rand.nextInt(6) + 1);
calculation += calculation / (rand.nextInt(6) + 1);
latency = System.nanoTime() - latency;
results[count++] = latency;
interval = (count / WARMUP * (PAUSE - 1)) + 1; // it will change to PAUSE when it reaches WARMUP
}
// now print the last (EXTRA * 2) results so you can compare before and after the pause change (from 1 to PAUSE)
System.out.println(new Date() + " => Calculation: " + calculation);
System.out.println("Results:");
long[] array = Arrays.copyOfRange(results, results.length - EXTRA * 2, results.length);
for(long t: array) System.out.println(t);
}
public static void main(String[] args) {
new JvmPauseLatency().run();
}
}
# 1 楼答案
TL;博士
http://www.brendangregg.com/activebenchmarking.html
问题N1。暂停更改后的第一次测量
看起来你面临的是on-stack replacement。当OSR发生时,VM暂停,目标函数的堆栈帧替换为等效帧
根案例是错误的微基准-它没有正确地预热。在while循环之前,只需在基准测试中插入以下行即可修复它:
如何检查-只需使用
-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+TraceNMethodInstalls
运行您的基准测试。我已经修改了您的代码-现在它会在每次调用之前将间隔打印到系统输出中:OSR通常发生在第4层,因此为了禁用它,您可以使用以下选项:
-XX:-TieredCompilation
禁用分层编译-XX:-TieredCompilation -XX:TieredStopAtLevel=3
禁用分层编译到4级-XX:+TieredCompilation -XX:TieredStopAtLevel=4 -XX:-UseOnStackReplacement
禁用OSR问题N2。如何测量
让我们从这篇文章开始。简言之:
nanoTime()
调用(请参见What is the cost of volatile write?)因此,为了避免所有这些陷阱,您可以使用基于JMH的基准测试,如下所示:
请注意,为了避免操作系统相关的影响,我已将忙循环实现更改为Blackhole#consumeCPU()。因此,我的结果是:
请注意,结果几乎是相加的,即latencyFunc+latencyBusyPauseShort=measureShort
问题N3。差距很大
你的考试怎么了?它不能正确地预热JVM,即它使用一个参数来预热,另一个参数来测试。为什么这很重要?JVM使用概要文件引导的优化,例如,它统计分支的执行频率,并为特定概要文件生成“最佳”(无分支)代码。因此,我们试图用参数1预热JVM我们的基准测试,JVM生成“最佳代码”,而while循环中的分支从未执行过。以下是来自JIT编译日志(
-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation
)的事件:属性更改后,JIT使用不常见的陷阱来处理非最佳代码。我已经创建了一个基准,它基于原始基准,并做了一些小改动:
vdso clock_gettime
,我们无法分析此代码)_
结果是
为了修复此基准,只需将
new JvmPauseLatency().run(0)
替换为new JvmPauseLatency().run(PAUSE);
,结果如下:如果您想动态地更改“暂停”,您必须动态地预热JVM,即
问题N4。Xint-口译员呢
在基于开关的解释器的情况下,我们有很多问题,主要是间接分支指令。我做了3个实验:
每个实验都是通过以下命令
sudo perf stat -e cycles,instructions,cache-references,cache-misses,bus-cycles,branch-misses java -Xint JvmPauseLatency
启动的,结果如下:在分支未命中的情况下,由于内存占用过大,延迟和占用空间呈非线性增长
# 2 楼答案
您可能无法依赖任何计时器的精度来获得您想要的精度,https://docs.oracle.com/javase/8/docs/api/java/lang/System.html#nanoTime 指出