【问题标题】:Java 8 odd timing/memory issueJava 8 奇数计时/内存问题
【发布时间】:2015-10-07 14:13:32
【问题描述】:

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

【问题讨论】:

  • 我无法重现这个。在这里按预期工作。
  • 如果您可以重现此问题,我建议您针对 JDK 提交错误。我只能猜测它与 JIT 的加入有关。出于兴趣:需要 new Double(...) 吗?这是我不希望在实际代码中找到的东西。 @PM77-1:将 1.0 的整数值存储在 double 中绝不会导致此类问题,因为它可以在不损失精度的情况下表示。如果== 给出错误结果,我宁愿期望由于某种原因使用了一个装箱值(由于 JVM 中的错误)。
  • 我可以重现这个(oracle java 1.8.0_60 on 64-bit linux machine)。
  • 从生产代码到这个小例子,追查问题一定是做了大量工作……
  • 这绝对是一个JIT优化bug。 -XX:-TieredCompilation-XX:-EliminateAllocations 可能是可接受的解决方法,不会显着降低性能。

标签: java memory jvm java-8 timing


【解决方案1】:

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

我下载并安装了JDK 9 early access build 83 并注意到该错误在那里消失了。但是在 JDK 9 early access build 82 中它仍然存在。 b82 和 b83 之间的 changelog 只显示了一个相关的错误修复(如果我错了,请纠正我):JDK-8134031“具有内联和转义分析的复杂代码的错误 JIT 编译”。提交的testcase有点类似:大循环,几个盒子(类似于我们测试中的单元素数组)导致盒子里面的值突然变化,所以结果就默默的不正确了(没有crash,没有异常) ,只是不正确的值)。在我们的案例中,据报道该问题在 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) 表示层级):

...
     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 的方法中。这里发生了一些不好的事情:可能在短时间内&lt;init&gt;@16 OSR 版本有效,然后被&lt;init&gt;@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)
...

因此,在这种情况下,两个 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 编译的。

【讨论】:

  • 伟大的工作。请将此包含在错误报告中,因为它可能有助于 JDK 开发人员解决问题。如果可以的话,我会给 +2... :-)
  • 是的,如果您的代码与性能相关的大型长时间运行方法(这种模式与典型的应用程序代码不匹配,而是典型的人工基准代码),则堆栈替换会有所帮助。
  • 哇,干得好!我已经提交了错误报告,它仍在审核中。假设我可以在/如果它被接受时向它添加信息,我当然会包括这个。再次感谢!
  • @bcothren,我编辑了答案。似乎问题有些不同,并且已经解决了。
【解决方案2】:

我可以使用发布在 http://www.javaspecialists.eu/archive/Issue234.html

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

【讨论】:

  • Azul 有 2 个 JVM,Zulu 和 Zing。从您提供的链接(已损坏)看来,您指的是 Zulu,而不是 Zing。 Zulu 是一个 OpenJDK 构建,它完全是 OpenJDK 代码,但经过测试和支持。对于可比较的版本,它应该表现出相同的行为。 Zing 完全不同。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-01-11
  • 2017-04-02
  • 1970-01-01
  • 2015-07-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多