Java 没有对象分配的for循环如何导致JVM抖动?
我一直在对下面的代码进行微基准测试,我注意到了一些有趣的东西,我希望有人能提供更多的信息。它会导致这样一种情况,即for循环可以继续快速运行,同时阻塞JVM中的其他线程。如果这是真的,那么我想理解为什么,如果这不是真的,那么任何对我可能缺失的东西的洞察都将被感激 为了建立这种情况,让我带您浏览一下我正在运行的基准及其结果 代码非常简单,迭代数组中的每个元素,对其内容求和。重复“targetCount”次Java 没有对象分配的for循环如何导致JVM抖动?,java,jvm,jvm-hotspot,microbenchmark,Java,Jvm,Jvm Hotspot,Microbenchmark,我一直在对下面的代码进行微基准测试,我注意到了一些有趣的东西,我希望有人能提供更多的信息。它会导致这样一种情况,即for循环可以继续快速运行,同时阻塞JVM中的其他线程。如果这是真的,那么我想理解为什么,如果这不是真的,那么任何对我可能缺失的东西的洞察都将被感激 为了建立这种情况,让我带您浏览一下我正在运行的基准及其结果 代码非常简单,迭代数组中的每个元素,对其内容求和。重复“targetCount”次 public class UncontendedByteArrayReadBM extend
public class UncontendedByteArrayReadBM extends Benchmark {
private int arraySize;
private byte[] array;
public UncontendedByteArrayReadBM( int arraySize ) {
super( "array reads" );
this.arraySize = arraySize;
}
@Override
public void setUp() {
super.setUp();
array = new byte[arraySize];
}
@Override
public void tearDown() {
array = null;
}
@Override
public BenchmarkResult invoke( int targetCount ) {
long sum = 0;
for ( int i=0; i<targetCount; i++ ) {
for ( int j=0; j<arraySize; j++ ) {
sum += array[j];
}
}
return new BenchmarkResult( ((long)targetCount)*arraySize, "uncontended byte array reads", sum );
}
}
在这个问题的末尾,我已经包含了如何测量抖动的代码
然而,有趣的是,它确实发生在“JVM预热”期间,因此不是“正常”,但我想更详细地了解以下内容:
2.4519521584902644 uncontended byte array reads/ns [maxJitter=2561.222ms totalTestRun=4078.383ms]
请注意,抖动超过2.5秒。通常我会把这记在GC上。但是,在测试运行之前,我确实触发了一个System.gc(),并且-XX:+PrintGCDetails此时不显示gc。事实上,在任何测试运行期间都没有GC,因为在对预先分配的字节进行求和的测试中几乎没有对象分配。每次我运行一个新的测试时,它都会发生,因此我并不怀疑它是来自随机发生的其他进程的干扰
我的好奇心猛增,因为当我注意到抖动非常高时,总的运行时间,以及每纳秒读取数组元素的次数几乎保持不变。因此,在这种情况下,一个线程在一个4核机器上严重滞后,而工作线程本身没有滞后,并且没有GC在进行
进一步调查后,我查看了Hotspot编译器的工作,并通过-XX:+PrintCompilation发现了以下内容:
2632 2% com.mosaic.benchmark.datastructures.array.UncontendedByteArrayReadBM::invoke @ 14 (65 bytes)
6709 2% made not entrant com.mosaic.benchmark.datastructures.array.UncontendedByteArrayReadBM::invoke @ -2 (65 bytes)
打印出来的这两行之间的延迟约为2.5秒。当包含big for循环的方法将其优化代码标记为不再是进入者时,就正确了
我的理解是Hotspot在后台线程上运行,当它准备好交换新版本的代码时,它会等待已经运行的代码到达安全点,然后再交换。对于位于每个循环体末端的大for循环(可能已经展开了一些)。我不希望有2.5s的延迟,除非这个交换必须在JVM上执行stop the world事件。在对以前编译的代码进行去优化时,它会这样做吗
因此,我要问JVM内部专家的第一个问题是,我在这方面走对了吗?2.5秒的延迟是否是由于将该方法标记为“非参赛者”造成的;如果是这样,为什么它会对其他线程产生如此巨大的影响?如果这不太可能是原因,那么关于调查其他方面的任何想法都将是非常好的
(为了完整起见,下面是我用来测量抖动的代码)
抖动的原因有很多
- 睡眠在毫秒级是非常不可靠的
- 上下文开关
- 打断
- 由于运行其他程序而导致的缓存未命中
顺便说一句:你所做的就是测量系统的抖动。这花了一些时间才找到冒烟的枪,但经验教训是有价值的;特别是如何证明和隔离原因。所以我觉得把它们记录在这里很好 JVM确实在等待执行停止世界事件。Alexey Ragozin在上有一篇关于这个话题的非常好的博客文章,这篇文章让我走上了正确的道路。他指出,安全点在JNI方法边界和Java方法调用上。因此,我这里的for循环中没有安全点 要理解Java中的停止世界事件,请使用以下JVM标志:
-XX:+PrintGCApplicationStoppedTime-XX:+PrintSafepointStatistics-XX:PrintSafepointStatisticsCount=1
第一个打印出停止世界活动的总持续时间,它不仅限于GC。在我的例子中,打印出:
Total time for which application threads were stopped: 2.5880809 seconds
这证明了我在线程等待到达安全点时遇到了问题。接下来的两个参数打印出JVM希望等待全局安全点到达的原因
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
4.144: EnableBiasedLocking [ 10 1 1 ] [ 2678 0 2678 0 0 ] 0
Total time for which application threads were stopped: 2.6788891 seconds
这说明JVM在尝试启用有偏锁定时等待了2678ms。为什么这是一个世界性的活动?谢天谢地,马丁·汤普森过去也遇到过这个问题,他已经记录了这个问题。事实证明,Oracle JVM在启动期间有相当多的线程争用,在这段时间内,有偏差的锁定成本非常高,因此它会将优化延迟四秒钟。所以这里发生的事情是,我的微基准测试超过了4秒,然后它的循环中没有安全点。因此,当JVM试图打开偏向锁定时,它必须等待
所有对我有效的候选解决方案包括:
同意,这个建议在过去的十五年里对我很有帮助:)但现在我渴望更深入地理解。我知道在Java中测量时间会遇到相当大的时钟粒度问题和延迟,System.currentTimeMillis()的延迟可能高达16或32毫秒,这就是为什么我对常见的.4ms抖动不感兴趣的原因。与此相比,2.5秒是主要的。我目前认为上下文切换和中断来自外部事件
Total time for which application threads were stopped: 2.5880809 seconds
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
4.144: EnableBiasedLocking [ 10 1 1 ] [ 2678 0 2678 0 0 ] 0
Total time for which application threads were stopped: 2.6788891 seconds