NullPointerException 被扔进了无法抛出的地方

我在一段无法抛出的代码中得到了一个NullPointerException。我开始认为在JRE中发现了一个错误。我使用javac 1.8.0_51作为编译器,问题发生在jre 1.8.0_45和最新的1.8.0_60中。

引发异常的行位于循环内部,该循环位于闭包 lambda 函数内部。我们在 spark 1.4 中运行这样的闭包。该行执行1-2百万次,并且我每运行3或4次,使用相同的输入,不是确定性地获得错误。

我在这里粘贴相关的代码段:

        JavaRDD .... mapValues(iterable -> {
                LocalDate[] dates = ...
                long[] dateDifferences = ...

                final double[] fooArray = new double[dates.length];
                final double[] barArray = new double[dates.length];
                for (Item item : iterable) {
                    final LocalDate myTime = item.getMyTime();
                    final int largerIndex = ...
                    if (largerIndex == 0) {
                        ...
                    } else if (largerIndex >= dates.length - 1) {
                        ...
                    } else {
                        final LocalDate largerDate = dates[largerIndex];
                        final long daysBetween = ...
                        if (daysBetween == 0) {
                            ...
                        } else {
                            double factor = ...
                            // * * * NULL POINTER IN NEXT LINE * * * //
                            fooArray[largerIndex - 1] += item.getFoo() * factor;
                            fooArray[largerIndex] += item.getFoo() * (1 - factor);
                            barArray[largerIndex - 1] += item.getBar() * factor;
                            barArray[largerIndex] += item.getBar() * (1 - factor);
                        }
                    }
                }
                return new NewItem(fooArray, barArray);
            })
            ...

我开始分析代码,发现:

  • fooArray永远不会为空,因为你上面有几行“新”
  • 较大索引是原始的
  • item 永远不会为 null,因为它在上面的几行中已经使用过
  • getFoo() 返回双精度,没有拆箱
  • 因子是原始的

我无法在本地运行相同的输入并对其进行调试:这是在 spark 群集上运行的。所以我在抛出线之前添加了一些调试println:

System.out.println("largerIndex: " + largerIndex);
System.out.println("foo: " + Arrays.toString(foo));
System.out.println("foo[1]: " + foo[1]);
System.out.println("largerIndex-1: " + (largerIndex-1));
System.out.println("foo[largerIndex]: " + foo[largerIndex]);
System.out.println("foo[largerIndex - 1]: " + foo[largerIndex - 1]);

这是输出:

largerIndex: 2
foo: [0.0, 0.0, 0.0, 0.0, ...]
foo[1]: 0.0
largerIndex-1: 1
foo[largerIndex]: 0.0
15/10/01 12:36:11 WARN scheduler.TaskSetManager: Lost task 0.0 in stage 7.0 (TID 17162, host13): java.lang.NullPointerException
    at my.class.lambda$mymethod$87560622$1(MyFile.java:150)
    at my.other.class.$$Lambda$306/764841389.call(Unknown Source)
    at org.apache.spark.api.java.JavaPairRDD$$anonfun$toScalaFunction$1.apply(JavaPairRDD.scala:1027)
    ...

所以 foo[largerIndex - 1] 目前正在抛出空指针。请注意,下面还会抛出它:

int idx = largerIndex - 1;
foo[idx] += ...;

但不是以下:

foo[1] += ....;

我看了一下类文件中的字节码,发现没有什么奇怪的。在iconst_1、isub 和 daload 之前,您在堆栈中正确地引用了 foo 和 largerIndex。

我只是发布这个是为了在思考jre错误之前收集想法。你们中是否有人使用火花遇到过同一类问题?或一般的 lambda 函数。是否可以使用一些调试标志运行jvm来帮助我理解这种奇怪的行为?或者我应该将问题提交给某个地方的人吗?


答案 1

在我看来,这与这里描述的问题(JIT问题)非常相似:http://kingsfleet.blogspot.com.br/2014/11/but 不可能或发现.html

您的观察结果,即它不是每次都发生,并且在阅读代码时“不可能”发生,这与那里描述的完全相同。要找到答案,请使用命令行选项从 JITed 中排除您的方法(您需要指定正确的类/方法名称):

-XX:CompileCommand=exclude,java/lang/String.indexOf

或者使用完全关闭它

-Xint

这可能太激烈了。


答案 2

推荐