输出 -1 在循环中变为斜杠

2022-08-31 19:39:48

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

/
-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

答案 1

这可以可靠地复制(或不复制,取决于你想要什么),OpenJDK(根据Oleksandr Pyrohov)和OpenJDK 13(根据Carlos Heuberger)。openjdk version "1.8.0_222"12.0.1

我运行了足够多的次数来获得这两种行为,这就是差异。-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)

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

此外,运行(解释或使用)与(强制编译在主线程中运行,而不是并行运行),输出是有保证的,并且30000次迭代打印出很多东西,所以编译器似乎是罪魁祸首。这可以通过运行 with 来确认,该操作会禁用并且不会产生输出(在级别 4 停止会再次显示 bug)。-XX:-TieredCompilationC2-XbatchC2-XX:TieredStopAtLevel=1C2

在正确执行中,首先使用级别 3 编译该方法,然后使用级别 4 编译该方法。

在 buggy 执行中,以前的编译被分解 (),然后再次在 Level 3 上编译(即 ,请参阅上一个链接)。made non entrantC1

因此,它绝对是 中的一个错误,尽管我不完全确定它返回到级别 3 编译的事实是否会影响它(以及为什么它会返回到级别 3,仍然存在许多不确定性)。C2

您可以使用以下行生成装配体代码,以更深入地进入兔子洞(另请参阅此图以启用装配体打印)。

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

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

很可能已经有一个关于此的错误报告,因为代码是由其他人提供给OP的,并且因为所有代码C2都不是没有错误的。我希望这一分析对其他人和我一样具有信息量。

正如尊敬的apangin在评论中指出的那样,这是最近的一个错误。非常有义务:)所有感兴趣和乐于助人的人


答案 2

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

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

...应始终导致 (8 - 3 = 5;5 - 3 = 2;2 - 3 = -1)。更奇怪的是,它从不以IDE的调试模式输出。i-1

有趣的是,当我在转换为之前添加检查的那一刻,那么没有问题...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. 某些编码标准指定字符串文本应为 的目标,而不是参数,从而最大程度地减少 NullPointerExceptions。.equals()

我让这种情况不发生的唯一方法是使用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表之间似乎确实存在一些对应关系。

  • 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处的字符是值,48 - 1 = 47(字符),依此类推...0/