bco*_*ren 24 java memory jvm timing java-8
我遇到了一个在运行Java 8时可以创建的一个相当奇怪的问题.问题表现在JVM本身内部发生了某种时序错误.它本质上是间歇性的,但很容易重现(至少在我的测试环境中).问题是显式设置的数组值在某些情况下被销毁并替换为0.0.具体来说,在下面的代码中,array[0]
在行之后评估为0.0 new Double(r.nextDouble());
.然后,如果您立即array[0]
再次查看其内容,它现在会将该值显示为正确的值1.0.运行此测试用例的示例输出是:
claims array[0] != 1.0....array[0] = 1.0
claims array[0] now == 1.0...array[0] = 1.0`
Run Code Online (Sandbox Code Playgroud)
我正在运行64位Windows 7,并且能够从Eclipse中以及从命令行编译时使用JDK 1.8_45,1.8_51和1.8_60重现此问题.我无法解决运行1.7_51的问题.在另一个64位Windows 7机箱上演示了相同的结果.
这个问题出现在一个大型的,非平凡的软件中,但我已经设法将其浓缩为几行代码.下面是一个小问题,用于演示此问题.这是一个相当奇怪的测试用例,但似乎都有必要导致错误.使用Random
不是必需的 - 我可以r.nextDouble()
用任何双重值替换所有并证明问题.有趣的是,如果someArray[0] = .45;
被替换someArray[0] = r.nextDouble();
,我无法复制问题(虽然没有什么特别的.45
).Eclipse调试也没有任何帮助 - 它会改变时间,使其不再发生.即使是陈述良好的System.err.println()
声明也会导致问题不再出现.
同样,问题是间歇性的,因此要重现问题,可能需要多次运行此测试用例.我认为在获得上面显示的输出之前,我必须运行它的次数大约是10次.在Eclipse中,我在运行后给它一两秒,然后在没有发生时将其杀死.从命令行开始 - 运行它,如果它没有发生CTRL+C
退出并再试一次.似乎如果它会发生,它会很快发生.
我在过去遇到过这样的问题,但它们都是线程问题.我无法弄清楚这里发生了什么 - 我甚至看过字节码(顺便说一下,1.7_51和1.8_45之间是相同的).
关于这里发生了什么的任何想法?
import java.util.Random;
public class Test {
Test(){
double array[] = new double[1];
Random r = new Random();
while(true){
double someArray[] = new double[1];
double someArray2 [] = new double [2];
for(int i = 0; i < someArray2.length; i++) {
someArray2[i] = r.nextDouble();
}
// for whatever reason, using r.nextDouble() here doesn't seem
// to show the problem, but the # you use doesn't seem to matter either...
someArray[0] = .45;
array[0] = 1.0;
// commented out lines also demonstrate problem
new Double(r.nextDouble());
// new Float(r.nextDouble();
// double d = new Double(.1) * new Double(.3);
// double d = new Double(.1) / new Double(.3);
// double d = new Double(.1) + new Double(.3);
// double d = new Double(.1) - new Double(.3);
if(array[0] != 1.0){
System.err.println("claims array[0] != 1.0....array[0] = " + array[0]);
if(array[0] != 1.0){
System.err.println("claims array[0] still != 1.0...array[0] = " + array[0]);
}else {
System.err.println("claims array[0] now == 1.0...array[0] = " + array[0]);
}
System.exit(0);
}else if(r.nextBoolean()){
array = new double[1];
}
}
}
public static void main(String[] args) {
new Test();
}
}
Run Code Online (Sandbox Code Playgroud)
Tag*_*eev 21
更新:似乎我的原始答案不正确,OnStackReplacement只是在这个特殊情况下揭示了问题,但最初的错误是在转义分析代码中.转义分析是一个编译器子系统,它确定对象是否从给定方法中逃脱.非转义对象可以进行标量化(而不是堆上分配)或完全优化.在我们的测试中,转义分析确实很重要,因为几个创建的对象肯定不会逃避该方法.
我下载并安装了JDK 9早期访问版本83,并注意到该bug在那里消失了.但是在JDK 9早期访问版本82中它仍然存在.b82和b83之间的更改日志只显示了一个相关的错误修复(如果我错了,请更正我):JDK-8134031 "内联和转义分析的复杂代码的JIT编译错误".提交的测试用例有点类似:大循环,几个框(类似于我们测试中的单元素数组)导致框内值突然改变,因此结果变得无声不正确(没有崩溃,没有异常,只是不正确的价值).在我们的案例中,据报道问题不会出现在8u40之前.该介绍了修复 非常简短:只需在逃逸分析源中进行一行更改.
根据OpenJDK错误跟踪器,该修复程序已经被移植到JDK 8u72分支,计划于2016年1月发布.似乎将此修复程序向后移动到即将到来的8u66已经太晚了.
建议的解决方法是禁用转义分析(-XX:-DoEscapeAnalysis)或禁用消除分配优化(-XX:-EliminateAllocations).因此@apangin 实际上比我更接近答案.
以下是原始答案
首先,我无法使用JDK 8u25重现问题,但可以在JDK 8u40和8u60上重复:有时它运行正常(卡在无限循环中),有时它输出和退出.因此,如果JDK降级到8u25是可以接受的,您可以考虑这样做.请注意,如果您需要在javac中进行后续修复(许多事情,特别是涉及lambdas的事情已在1.8u40中修复),您可以使用较新的javac进行编译,但可以在较旧的JVM上运行.
对我来说,似乎这个特殊问题可能是OnStackReplacement机制中的一个错误(当OSR发生在第4层时).如果您不熟悉OSR,可以阅读此答案.OSR肯定会出现在你的情况下,但有点奇怪.下面是-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+TraceNMethodInstalls
对失败的运行(%
指OSR JIT,@ 28
是指OSR字节码的位置,(3)
并(4)
意味着层级):
...
91 37 % 3 Test::<init> @ 28 (194 bytes)
Installing osr method (3) Test.<init>()V @ 28
93 38 3 Test::<init> (194 bytes)
Installing method (3) Test.<init>()V
94 39 % 4 Test::<init> @ 16 (194 bytes)
Installing osr method (4) Test.<init>()V @ 16
102 40 % 4 Test::<init> @ 28 (194 bytes)
103 39 % 4 Test::<init> @ -2 (194 bytes) made not entrant
...
Installing osr method (4) Test.<init>()V @ 28
113 37 % 3 Test::<init> @ -2 (194 bytes) made not entrant
claims array[0] != 1.0....array[0] = 1.0
claims array[0] now == 1.0...array[0] = 1.0
Run Code Online (Sandbox Code Playgroud)
因此,tier4的OSR发生在两个不同的字节码偏移中:偏移16(这是while
循环入口点)和偏移28(它是嵌套的for
循环入口点).似乎在您的方法的两个OSR编译版本之间的上下文传输期间发生某些竞争条件,这导致上下文中断.当执行切换到OSR方法,应该传输的当前上下文包括局部变量等的值array
和r
进OSR'ed方法.这里发生了一些不好的事情:可能是很短的时间<init>@16
OSR版本工作,然后它被替换为<init>@28
,但上下文有一点延迟更新.OSR上下文传输可能会干扰"消除分配"优化(正如@apangin所说,关闭此优化有助于您的情况).我的专业知识还不足以在这里深入挖掘,可能@apangin可能会发表评论.
相比之下,在正常运行中,只创建并安装了第4层OSR方法的一个副本:
...
Installing method (3) Test.<init>()V
88 43 % 4 Test::<init> @ 28 (194 bytes)
Installing osr method (4) Test.<init>()V @ 28
100 40 % 3 Test::<init> @ -2 (194 bytes) made not entrant
4592 44 3 java.lang.StringBuilder::append (8 bytes)
...
Run Code Online (Sandbox Code Playgroud)
所以看起来在这种情况下,两个OSR版本之间不会发生竞争,一切都运行良好.
如果将外部循环体移动到单独的方法,问题也会消失:
import java.util.Random;
public class Test2 {
private static void doTest(double[] array, Random r) {
double someArray[] = new double[1];
double someArray2[] = new double[2];
for (int i = 0; i < someArray2.length; i++) {
someArray2[i] = r.nextDouble();
}
... // rest of your code
}
Test2() {
double array[] = new double[1];
Random r = new Random();
while (true) {
doTest(array, r);
}
}
public static void main(String[] args) {
new Test2();
}
}
Run Code Online (Sandbox Code Playgroud)
手动展开嵌套for
循环也会删除错误:
int i=0;
someArray2[i++] = r.nextDouble();
someArray2[i++] = r.nextDouble();
Run Code Online (Sandbox Code Playgroud)
为了解决这个问题,你似乎应该在同一个方法中至少有两个嵌套循环,因此OSR可以出现在不同的字节码位置.因此,对于特定代码段中的解决问题,您可以执行相同的操作:将循环体提取到单独的方法中.
另一种解决方案是完全禁用OSR -XX:-UseOnStackReplacement
.它在生产代码中很少有用.循环计数器仍然有效,如果你的方法有多次迭代循环被调用至少两次,第二次运行将是JIT编译的.即使你的长循环方法由于禁用OSR而没有进行JIT编译,它调用的任何方法仍然是JIT编译的.