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


54

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

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

2
评论不作进一步讨论;此对话已转移至聊天
塞缪尔·柳

Answers:


36

可以(根据openjdk version "1.8.0_222"我的分析使用),OpenJDK 12.0.1(根据Oleksandr Pyrohov)和OpenJDK 13(根据Carlos Heuberger)可靠地复制(或复制,取决于您想要的内容)。

我运行了-XX:+PrintCompilation足够的时间来获得两种行为的代码,这就是区别。

Buggy实现(显示输出):

 --- 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(这迫使编译在主线程中运行,而不是平行),输出保证,并与30000反复打印出了很多东西,所以C2编译器似乎成为罪魁祸首。通过运行可以确认这一点-XX:TieredStopAtLevel=1,它禁用C2并且不产生输出(在4级停止再次显示该错误)。

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

在越野车执行中,先前的编译将被舍弃(made non entrant),然后再次在Level 3上进行编译(即C1,请参见前面的链接)。

因此,这绝对是一个错误C2,尽管我不确定要返回到3级编译的事实是否会对其造成影响(以及为什么返回到3级,仍然存在很多不确定性)。

您可以生成以下行的汇编代码去更深进了兔子洞(另见使装配打印)。

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

在这一点上,我开始精疲力尽,当放弃以前的编译版本时,越野车的行为开始显现出来,但是我具备90年代的少量组装技能,所以我会让比我聪明的人来使用它从这里。

由于该代码是由其他人提供给OP的,而且所有C2代码都没有bug,因此很可能已经有关于此问题的bug报告。我希望这种分析对我一样有益。

正如古老的apangin在评论中指出的,这是一个最近的bug。所有有兴趣和乐于助人的人都有义务:)


我也认为是C2-使用JitWatch查看了生成的汇编代码(并试图理解它)- C1生成的代码仍然类似于字节码,C2完全不同(我什至找不到i8的初始化)
user85421-Baned

您的回答非常好,我尝试了禁用c2,结果是正确的。但是,通常,这些参数中的大多数都是项目中的默认值,尽管实际项目中没有上述代码,但是很可能具有类似的代码,如果项目使用类似的代码,那确实很糟糕
okali

1
@Eugene这是一个非常棘手的问题,我确定它将是类似eclipse编译器错误或类似的问题……而且我一开始也无法复制它
。.– Kayaman

1
@Kayaman同意。您所做的分析非常好,应该足以让apangin解释和解决此问题。火车上多么美妙的早晨!
尤金(Eugene),

7
我只是偶然地注意到了这个话题。为了确保我能看到问题,请使用@mentions或添加#jvm标记。好的分析,顺便说一句。这确实是C2编译器错误,仅在几天前已修复-JDK-8231988
apangin

4

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

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. 一些编码标准指定String文字应作为的目标.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表之间似乎确实存在某些对应关系。

  • 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(字符/),依此类推...


1
他字符“ /”的数值是“ -1”?这是哪里来的?((int)'/' == 47; (char)-1未定义0xFFFF为Unicode中的<非字符>)
user85421-Banned,

1
char c ='/'; int a = Character.getNumericValue(c); System.out.println(a);
Ambro-r 9:04

getNumericValue()与给定的代码有什么关系???以及如何转换-1'/'??? 为什么不去'-'getNumericValue('-')也是-1??? (顺便说一句,很多方法都返回了-1
user85421-Banned

@CarlosHeuberger,我跑getNumericValue()value/)来获取字符值。您100%正确地认为ASCII十进制值/应为47(这也是我所期望的值),但是getNumericValue()如我所添加的那样,此时返回-1 System.out.println(Character.getNumericValue(value.toCharArray()[0]));。我可以看到您所指的困惑,并更新了帖子。
Ambro-r

1

不知道Java为什么会给出这样的随机输出,但是问题出在您的串联中,对于较大ifor循环内部值,串联失败。

如果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。


1
我不认为字符串连接是本机的-它只使用StringConcatFactory(OpenJDK 13)或StringBuilder(Java 8)
user85421-Banned

@CarlosHeuberger也可能。我认为它必须来自Java 9 StringConcatFactory 。但据我所知,直到Java 8 java不支持运算符重载
Vinay Prajapati

@Vinay,也尝试过此方法,是的,但是它起作用了,但是当您将循环数从30000增加到3000000时,您开始遇到同样的问题。
Ambro-r

@ Ambro-r我尝试使用您的建议值,但Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1出现错误。奇怪。
Vinay Prajapati,

3
i + ""完全按照new StringBuilder().append(i).append("").toString()Java 8进行编译,并最终使用它生成输出
user85421-Banned 19'Oct
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.