Java 8 奇数计时/内存问题

标签 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()具有任何双重值(value)并证明问题。有趣的是,如果 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();
    }
}

最佳答案

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

我下载安装了JDK 9 early access build 83并注意到错误在那里消失了。但是在 JDK 9 早期访问版本 82 中它仍然存在。 changelog b82 和 b83 之间只显示了一个相关的错误修复(如果我错了,请纠正我):JDK-8134031 “使用内联和转义分析对复杂代码进行不正确的 JIT 编译”。 promise testcase有点类似:大循环,几个盒子(类似于我们测试中的单元素数组)导致盒子内部的值突然变化,所以结果变得无声无息(没有崩溃,没有异常(exception),只是值不正确) .与我们的情况一样,据报道在 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 中进行修复(特别是涉及 lambda 的许多事情在 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

因此,tier4 的 OSR 发生在两个不同的字节码偏移量:偏移量 16(即 while 循环入口点)和偏移量 28(即嵌套的 for 循环入口点)。似乎在您的方法的两个 OSR 编译版本之间的上下文传输期间发生了一些竞争条件,这会导致上下文中断。当执行移交给 OSR 方法时,它应该转移当前上下文,包括局部变量的值,如 arrayr进入 OSR 的方法。这里发生了一些不好的事情:可能是很短的时间 <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)
...

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

关于Java 8 奇数计时/内存问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32994608/

相关文章:

java - 需要帮助编写正则表达式

java - Spring Batch RowMapper - 获取列数和类型

performance - 如何将我的结构变量放入 CPU 缓存以消除主内存页面访问时间?选项

python - 带 yield 的函数使用多少内存?

将 char* 内存地址转换为 void* 并返回 C

java - Java 中打开的文件太多

java - 创建对象的数学运算符?

java - 为什么我的数据库连接在尝试执行 JOOQ 生成的 SQL 时关闭?

java - JVM如何处理动态类

基于 JVM 堆内存的 Kubernetes HPA