Java 8奇数时序/内存问题

时间:2015-10-07 14:13:33

标签: java memory jvm java-8 timing

我遇到了一个在运行Java 8时可以创建的一个相当奇怪的问题。问题表现为JVM本身内发生了某种时序错误。它本质上是间歇性的,但很容易重现(至少在我的测试环境中)。问题是显式设置的数组值在某些情况下被销毁并替换为0.0。具体来说,在下面的代码中,array[0]在行new Double(r.nextDouble());之后评估为0.0。然后,如果您再次立即查看array[0]的内容,它现在会将值显示为正确的值1.0。运行此测试用例的示例输出是:

claims array[0] != 1.0....array[0] = 1.0
claims array[0] now == 1.0...array[0] = 1.0`

我正在运行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();
    }
}

2 个答案:

答案 0 :(得分:21)

更新:似乎我的原始答案不正确,OnStackReplacement只是在这个特殊情况下发现了问题,但最初的错误是在转义分析代码中。转义分析是一个编译器子系统,它确定对象是否从给定方法中逃脱。非转义对象可以进行标量化(而不是堆上分配)或完全优化。在我们的测试中,逃逸分析确实很重要,因为几个创建的对象肯定不会逃脱该方法。

我下载并安装了JDK 9 early access build 83并注意到该错误消失了。但是在JDK 9早期访问版本82中它仍然存在。 b82和b83之间的changelog只显示了一个相关的错误修复(如果我错了,请更正我):JDK-8134031&#34;错误的JIT编译复杂代码,内联和转义分析&#34 ;。提交的testcase有点相似:大循环,几个框(类似于我们测试中的单元素数组)导致框内值突然改变,因此结果变得无声不正确(没有崩溃,没有例外,只是不正确的价值)。在我们的案例中,它报告说问题在8u40之前没有出现。 introduced fix非常短:转义分析源只有一行更改。

根据OpenJDK错误跟踪器,修复已经backported到JDK 8u72分支,这是scheduled将于2016年1月发布。似乎将此修复程序向后移植已经太晚了即将到来的8u66

建议的解决方法是禁用转义分析(-XX:-DoEscapeAnalysis)或禁用消除分配优化(-XX:-EliminateAllocations)。因此@apangin was actually closer得到了答案而不是我。

以下是原始答案

首先,我不能用JDK 8u25重现问题,但可以在JDK 8u40和8u60上重复:有时它运行正常(卡在无限循环中),有时输出和退出。因此,如果JDK降级到8u25是可以接受的,您可以考虑这样做。请注意,如果您需要在javac中进行后续修复(许多事情,特别是涉及lambdas的事情已在1.8u40中修复),您可以使用较新的javac进行编译,但可以在较旧的JVM上运行。

对我来说,似乎这个特殊问题可能是OnStackReplacement机制中的一个错误(当OSR发生在第4层时)。如果您不熟悉OSR,可以阅读this answer。 OSR肯定会出现在你的情况下,但有点奇怪。这里-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+TraceNMethodInstalls表示失败的运行(%表示OSR JIT,@ 28表示OSR字节码位置,(3)(4)表示层级别:< / p>

...
     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

因此,tier4的OSR发生两个不同的字节码偏移:偏移16(这是while循环入口点)和偏移28(它是嵌套的for循环入口点)。似乎在您的方法的两个OSR编译版本之间的上下文传输期间发生某些竞争条件,这导致上下文中断。当执行被移交给OSR方法时,它应该将当前上下文(包括arrayr等局部变量的值传递到OSR方法中。这里发生了一些不好的事情:可能在短时间<init>@16 OSR版本有效,然后用<init>@28替换,但上下文稍有更新。 OSR上下文传输可能会干扰&#34;消除分配&#34;优化(如@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)
...

看来在这种情况下,两个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();
    }
}

同时手动展开嵌套的for循环可以删除错误:

int i=0;
someArray2[i++] = r.nextDouble();
someArray2[i++] = r.nextDouble();

要发现这个错误,似乎你应该在同一个方法中至少有两个嵌套循环,因此OSR可以出现在不同的字节码位置。因此,对于特定代码段中的解决问题,您可以执行相同的操作:将循环体提取到单独的方法中。

另一种解决方案是使用-XX:-UseOnStackReplacement完全禁用OSR。它很少有助于生产代码。循环计数器仍然有效,如果你的方法有多次迭代循环被调用至少两次,第二次运行将是JIT编译的。即使你的长循环方法由于禁用OSR而没有进行JIT编译,它调用的任何方法仍然是JIT编译的。

答案 1 :(得分:0)

我可以在Zulu(OpenJDK的认证版本)中使用发布的代码重现此错误 http://www.javaspecialists.eu/archive/Issue234.html

使用oracle VM,我只能在Zulu中运行代码后重现此错误。看起来祖鲁污染了共享查找缓存。这种情况下的解决方案是使用-XX运行代码:-EnableSharedLookupCache。