Java 8 奇数计时/内存问题

2022-09-01 21:12:38

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

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

我运行的是 64 位 Windows 7,并且能够从 Eclipse 内部和从命令行编译时重现此问题,使用 JDKs 1.8_45、1.8_51 和 1.8_60。我无法在运行1.7_51时产生问题。在另一个 64 位 Windows 7 机箱上也演示了相同的结果。

这个问题出现在一个大型的,不平凡的软件中,但我设法将其压缩为几行代码。下面是一个演示该问题的小测试用例。这是一个看起来相当奇怪的测试用例,但似乎都是导致错误所必需的。不需要使用 - 我可以用任何双精度值替换所有值并演示问题。有趣的是,如果替换为 ,我无法复制该问题(尽管没有什么特别之处)。Eclipse调试也无济于事 - 它改变了时间,使其不再发生。即使一个位置恰当的声明也会导致问题不再出现。Randomr.nextDouble()someArray[0] = .45;someArray[0] = r.nextDouble();.45System.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();
    }
}

答案 1

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

我下载并安装了JDK 9抢先体验版本83,并注意到该错误在那里消失了。然而,在JDK 9抢先体验版82中,它仍然存在。b82和b83之间的更改日志只显示了一个相关的错误修复(如果我错了,请纠正我):JDK-8134031“带有内联和转义分析的复杂代码的错误JIT编译”。提交的测试用例有点类似:大循环,几个盒子(类似于我们测试中的单元素数组),导致盒子内值的突然变化,所以结果变得默默地不正确(没有崩溃,没有异常,只是不正确的值)。与我们的情况一样,据报道,在8u40之前,问题不会出现。引入的修复程序非常简短:只是逃逸分析源中的一行更改。

根据OpenJDK错误跟踪器,该修复程序已经向后移植到JDK 8u72分支,该分支计划于2016年1月发布。似乎将此修复程序向后移植到即将推出的8u66上为时已晚。

建议的解决方法是禁用转义分析 (-XX:-DoEscapeAnalysis) 或禁用消除分配优化 (-XX:-消除分配)。因此,@apangin实际上比我更接近答案。

以下是原始答案


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

对我来说,这个特殊问题似乎是OnStackReplacement机制中的一个错误(当OSR发生在第4层时)。如果您不熟悉OSR,可以阅读此答案。OSR肯定会发生在您的情况中,但以一种有点奇怪的方式。下面是失败的运行(表示 OSR JIT,表示 OSR 字节码位置,表示层级别):-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+TraceNMethodInstalls%@ 28(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(即循环入口点)和偏移量 28(嵌套的循环入口点)。在方法的两个 OSR 编译版本之间的上下文传输期间,似乎会发生一些争用情况,从而导致上下文中断。当执行移交给OSR方法时,它应该将当前上下文(包括局部变量的值)转换为OSR的方法。这里发生了一些不好的事情:可能在短时间内OSR版本工作,然后它被替换为,但是上下文更新了一点延迟。OSR 上下文传输可能会干扰“消除分配”优化(如@apangin所指出的,关闭此优化对您的情况有帮助)。我的专业知识不足以在这里进一步挖掘,可能@apangin评论。whileforarrayr<init>@16<init>@28

相比之下,在正常运行中,仅创建并安装第 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可以出现在不同的字节码位置。因此,要解决特定代码段中的问题,您可以执行相同的操作:将循环体提取到单独的方法中。

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


答案 2

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

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