Java 8 奇 timing/memory 问题

Java 8 odd timing/memory issue

我 运行 遇到了一个相当奇怪的问题,我可以在 运行 宁 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`

我 运行ning 64 位 Windows 7 并且能够重现这个问题,无论是在 Eclipse 中还是从命令行编译时,使用 JDKs 1.8_45、1.8_51 和 1.8_60。我无法产生问题 运行ning 1.7_51。同样的结果在另一个 64 位 Windows 7 box 上得到了证明。

这个问题出现在一个大型的、非平凡的软件中,但我设法将它压缩为几行代码。下面是一个演示该问题的小测试用例。这是一个看起来很奇怪的测试用例,但似乎是导致错误的必要条件。不需要使用 Random - 我可以用任何双精度值替换所有 r.nextDouble() 并演示问题。有趣的是,如果将 someArray[0] = .45; 替换为 someArray[0] = r.nextDouble();,我无法重现该问题(尽管 .45 没有什么特别之处)。 Eclipse 调试也无济于事——它改变了足够的时间,这样它就不会再发生了。即使放置得当的 System.err.println() 语句也会导致问题不再出现。

同样,该问题是间歇性的,因此要重现该问题,可能需要多次 运行 此测试用例。我认为我最多需要 运行 大约 10 次才能得到上面显示的输出。在 Eclipse 中,我在 运行ning 之后给它一两秒钟,然后如果它没有发生就将其杀死。从命令行相同 - 运行 它,如果它没有发生 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();
    }
}

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

我下载安装了 JDK 9 early access build 83 and noticed that the bug disappears there. However in JDK 9 early access build 82 it still exists. The changelog between b82 and b83 shows only one relevant bug fix (correct me if I'm wrong): JDK-8134031 "Incorrect JIT compilation of complex code with inlining and escape analysis". The committed testcase is somewhat similar: big loop, several boxes (similar to one-element arrays in our test) which lead to the sudden change of the value inside the box, so the result becomes silently incorrect (no crash, no exception, just incorrect value). As in our case it's reported that problem does not appear prior to 8u40. The introduced fix 很短:在越狱分析源中只改了一行。

根据 OpenJDK bug tracker,修复已经 backported to JDK 8u72 branch, which is scheduled to be released in January, 2016. Seems that it was too late to backport this fix to the upcoming 8u66

建议的解决方法是禁用逃逸分析 (-XX:-DoEscapeAnalysis) 或禁用消除分配优化 (-XX:-EliminateAllocations)。因此@apangin 比我的答案。

以下为原答案


首先,我无法用 JDK 8u25 重现问题,但可以在 JDK 8u40 和 8u60 上重现:有时 运行s 正确(卡在无限循环中),有时它输出和退出。所以如果 JDK 降级到 8u25 对你来说是可以接受的,你可以考虑这样做。请注意,如果您以后需要在 javac 中进行修复(许多问题,尤其是涉及 lambda 的问题已在 1.8u40 中修复),您可以使用较新的 javac 进行编译,但 运行 在较旧的 JVM 上。

对我来说,这个特殊问题似乎是 OnStackReplacement mechanism (when OSR occurs at tier 4). If you're unfamiliar with OSR, you may read this answer 中的错误。 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

因此,第 4 层的 OSR 出现在两个不同的字节码偏移量中:偏移量 16(这是 while 循环入口点)和偏移量 28(这是嵌套的 for 循环入口点)。似乎在您的方法的两个 OSR 编译版本之间的上下文传输期间发生了一些竞争条件,这导致上下文中断。当执行移交给 OSR 方法时,它应该将当前上下文(包括 arrayr 等局部变量的值)传输到 OSR'ed 方法中。这里发生了一些不好的事情:可能在短时间内 <init>@16 OSR 版本有效,然后它被替换为 <init>@28,但上下文更新有一点延迟。 OSR 上下文传输很可能会干扰 "eliminate allocations" 优化(正如@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 编译。

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

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