输出 -1 成为循环中的斜线

The output -1 becomes a slash in the loop

令人惊讶的是,以下代码输出:

/
-1

代码:

public class LoopOutPut {

    public static void main(String[] args) {
        LoopOutPut loopOutPut = new LoopOutPut();
        for (int i = 0; i < 30000; i++) {
            loopOutPut.test();
        }

    }

    public void test() {
        int i = 8;
        while ((i -= 3) > 0) ;
        String value = i + "";
        if (!value.equals("-1")) {
            System.out.println(value);
            System.out.println(i);
        }
    }

}

我尝试了很多次来确定这种情况会发生多少次,但不幸的是,最终还是不确定,而且我发现-2的输出有时会变成一个句号。另外,我也试过去掉while循环,输出-1,没有任何问题。谁能告诉我为什么?


JDK版本信息:

HopSpot 64-Bit 1.8.0.171
IDEA 2019.1.1

可以使用 openjdk version "1.8.0_222"(在我的分析中使用)、OpenJDK 12.0.1(根据 Oleksandr Pyrohov)和 OpenJDK 13(根据 Carlos Heuberger 的说法)。

我 运行 代码 -XX:+PrintCompilation 足够多次获得两种行为,这里是不同之处。

错误的实现(显示输出):

 --- Previous lines are identical in both
 54   17       3       java.lang.AbstractStringBuilder::<init> (12 bytes)
 54   23       3       LoopOutPut::test (57 bytes)
 54   18       3       java.lang.String::<init> (82 bytes)
 55   21       3       java.lang.AbstractStringBuilder::append (62 bytes)
 55   26       4       java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)
 55   20       3       java.lang.StringBuilder::<init> (7 bytes)
 56   19       3       java.lang.StringBuilder::toString (17 bytes)
 56   25       3       java.lang.Integer::getChars (131 bytes)
 56   22       3       java.lang.StringBuilder::append (8 bytes)
 56   27       4       java.lang.String::equals (81 bytes)
 56   10       3       java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)   made not entrant
 56   28       4       java.lang.AbstractStringBuilder::append (50 bytes)
 56   29       4       java.lang.String::getChars (62 bytes)
 56   24       3       java.lang.Integer::stringSize (21 bytes)
 58   14       3       java.lang.String::getChars (62 bytes)   made not entrant
 58   33       4       LoopOutPut::test (57 bytes)
 59   13       3       java.lang.AbstractStringBuilder::append (50 bytes)   made not entrant
 59   34       4       java.lang.Integer::getChars (131 bytes)
 60    3       3       java.lang.String::equals (81 bytes)   made not entrant
 60   30       4       java.util.Arrays::copyOfRange (63 bytes)
 61   25       3       java.lang.Integer::getChars (131 bytes)   made not entrant
 61   32       4       java.lang.String::<init> (82 bytes)
 61   16       3       java.util.Arrays::copyOfRange (63 bytes)   made not entrant
 61   31       4       java.lang.AbstractStringBuilder::append (62 bytes)
 61   23       3       LoopOutPut::test (57 bytes)   made not entrant
 61   33       4       LoopOutPut::test (57 bytes)   made not entrant
 62   35       3       LoopOutPut::test (57 bytes)
 63   36       4       java.lang.StringBuilder::append (8 bytes)
 63   18       3       java.lang.String::<init> (82 bytes)   made not entrant
 63   38       4       java.lang.StringBuilder::append (8 bytes)
 64   21       3       java.lang.AbstractStringBuilder::append (62 bytes)   made not entrant

更正运行(无显示):

 --- Previous lines identical in both
 55   23       3       LoopOutPut::test (57 bytes)
 55   17       3       java.lang.AbstractStringBuilder::<init> (12 bytes)
 56   18       3       java.lang.String::<init> (82 bytes)
 56   20       3       java.lang.StringBuilder::<init> (7 bytes)
 56   21       3       java.lang.AbstractStringBuilder::append (62 bytes)
 56   26       4       java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)
 56   19       3       java.lang.StringBuilder::toString (17 bytes)
 57   22       3       java.lang.StringBuilder::append (8 bytes)
 57   24       3       java.lang.Integer::stringSize (21 bytes)
 57   25       3       java.lang.Integer::getChars (131 bytes)
 57   27       4       java.lang.String::equals (81 bytes)
 57   28       4       java.lang.AbstractStringBuilder::append (50 bytes)
 57   10       3       java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)   made not entrant
 57   29       4       java.util.Arrays::copyOfRange (63 bytes)
 60   16       3       java.util.Arrays::copyOfRange (63 bytes)   made not entrant
 60   13       3       java.lang.AbstractStringBuilder::append (50 bytes)   made not entrant
 60   33       4       LoopOutPut::test (57 bytes)
 60   34       4       java.lang.Integer::getChars (131 bytes)
 61    3       3       java.lang.String::equals (81 bytes)   made not entrant
 61   32       4       java.lang.String::<init> (82 bytes)
 62   25       3       java.lang.Integer::getChars (131 bytes)   made not entrant
 62   30       4       java.lang.AbstractStringBuilder::append (62 bytes)
 63   18       3       java.lang.String::<init> (82 bytes)   made not entrant
 63   31       4       java.lang.String::getChars (62 bytes)

我们可以注意到一个显着差异。通过正确的执行,我们编译了 test() 两次。开始时一次,之后再一次(大概是因为 JIT 注意到该方法有多热)。在错误执行中 test() 被编译(或反编译)5 次。

此外,运行 -XX:-TieredCompilation(解释或使用 C2-Xbatch(强制在主线程中编译到 运行,而不是并行),输出是 gua运行teed 并且 30000 次迭代打印出很多东西,所以C2 编译器似乎是罪魁祸首。 运行 -XX:TieredStopAtLevel=1 证实了这一点,它禁用了 C2 并且不产生输出(停止在级别 4 再次显示错误)。

正确的执行方法是先用Level 3编译,再用Level 4编译。

在 buggy 执行中,之前的编译被丢弃 (made non entrant) 并在级别 3 上再次编译 (即 C1,参见之前的 link)。

所以它肯定是 C2 中的一个错误,虽然我不确定它回到 3 级编译的事实是否会影响它(以及为什么它回到 3 级,所以仍有许多不确定性)。

您可以使用以下行生成汇编代码以更深入地研究兔子洞(另请参阅 this 以启用汇编打印)。

java -XX:+PrintCompilation -Xbatch -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly LoopOutPut > broken.asm

在这一点上我开始运行技能不足,当以前的编译版本被丢弃时,错误行为开始表现出来,但我的组装技能是 90 年代的,所以我让比我聪明的人从这里拿走它。

很可能已经有关于此的错误报告,因为代码是由其他人提交给 OP 的,并且所有代码都是 C2 isn't without bugs。我希望这个分析对其他人和我一样有用。

正如 apangin 在评论中指出的那样,这是一个 recent bug。非常感谢所有感兴趣和乐于助人的人:)

不知道为什么 Java 会给出这样的随机输出,但问题出在您的连接中,在 for 循环中,i 的较大值会失败。

如果您将 String value = i + ""; 行替换为 String value = String.valueOf(i) ;,您的代码将按预期工作。

使用 + 将 int 转换为字符串的连接是原生的,可能存在错误(奇怪的是我们现在可能正在创建它)并导致此类问题。

注意:我将 for 循环中 i 的值减少到 10000,我没有遇到 + 连接的问题。

必须将此问题报告给 Java 利益相关者,他们可以就此发表意见。

编辑 我将for循环中i的值更新为300万,看到了一组新的错误如下:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1
    at java.lang.Integer.getChars(Integer.java:463)
    at java.lang.Integer.toString(Integer.java:402)
    at java.lang.String.valueOf(String.java:3099)
    at solving.LoopOutPut.test(LoopOutPut.java:16)
    at solving.LoopOutPut.main(LoopOutPut.java:8)

我的Java版本是8.

老实说,这很奇怪,因为从技术上讲,该代码应该永远不会输出,因为...

int i = 8;
while ((i -= 3) > 0);

... 应始终导致 i-1(8 - 3 = 5;5 - 3 = 2;2 - 3 = -1)。更奇怪的是它从不在我的 IDE.

的调试模式下输出

有趣的是,当我在转换为 String 之前添加检查时,没有问题...

public void test() {
  int i = 8;
  while ((i -= 3) > 0);
  if(i != -1) { System.out.println("Not -1"); }
  String value = String.valueOf(i);
  if (!"-1".equalsIgnoreCase(value)) {
    System.out.println(value);
    System.out.println(i);
  }
}

只有两点良好的编码习惯......

  1. 而是使用String.valueOf()
  2. 一些编码标准指定字符串文字应该是 .equals() 的目标,而不是参数,从而最大限度地减少 NullPointerExceptions。

我避免这种情况发生的唯一方法是使用 String.format()

public void test() {
  int i = 8;
  while ((i -= 3) > 0);
  String value = String.format("%d", i);
  if (!"-1".equalsIgnoreCase(value)) {
    System.out.println(value);
    System.out.println(i);
  }
}

...本质上看起来 Java 需要一点时间来喘口气 :)

编辑:这可能完全是巧合,但打印出的值与 ASCII Table 之间似乎确实存在某种对应关系。

  • i=-1,显示的字符为/(ASCII十进制值为47)
  • i = -2,显示的字符为.(ASCII十进制值为46)
  • i = -3,显示的字符为-(ASCII十进制值为45)
  • i=-4,显示的字符为,(ASCII十进制值为44)
  • i = -5,显示的字符为+(ASCII十进制值为43)
  • i=-6,显示的字符为*(ASCII十进制值为42)
  • i=-7,显示的字符为)(ASCII十进制值为41)
  • i=-8,显示的字符为((ASCII十进制值为40)
  • i=-9,显示的字符为'(ASCII十进制值为39)

真正有趣的是ASCII十进制48处的字符是值0和48 - 1 = 47(字符/)等...