놀랍게도 다음 코드가 출력됩니다.
/
-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 번 컴파일 (또는 디 컴파일) 됩니다.
또한 with -XX:-TieredCompilation
(해석 또는 사용 C2
) 또는 with -Xbatch
(컴파일을 병렬 대신 메인 스레드에서 강제로 실행)로 실행하면 출력이 보장 되고 30000 회 반복하면 많은 내용이 인쇄되므로 C2
컴파일러가 범인이되기 위해 이것은로 실행하여 확인되며 -XX:TieredStopAtLevel=1
, C2
출력 을 비활성화 하고 생성하지 않습니다 (레벨 4에서 중지하면 다시 버그가 표시됨).
올바른 실행에서 메소드는 먼저 레벨 3 컴파일로 컴파일 된 후 레벨 4로 컴파일됩니다.
버기 실행에서 이전 컴파일은 무시되고 ( made non entrant
) 레벨 3에서 다시 컴파일됩니다 (즉 C1
, 이전 링크 참조).
C2
레벨 3 컴파일로 되돌아 간다는 사실이 그 영향을 받는지 (그리고 왜 레벨 3으로 돌아가는지, 여전히 많은 불확실성이 있는지) 확실하지 않지만 확실하게 버그입니다 .
다음 줄을 사용하여 조립품 코드를 생성하여 토끼 구멍으로 깊숙이 들어갈 수 있습니다 (조립품 인쇄를 활성화 하려면 이 내용도 참조하십시오 ).
java -XX:+PrintCompilation -Xbatch -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly LoopOutPut > broken.asm
이 시점에서 기술이 부족해지기 시작합니다. 이전 컴파일 된 버전을 버릴 때 버그 동작이 나타나기 시작하지만 90 년대의 조립 기술이 거의 없기 때문에 나보다 똑똑한 사람을 보자 여기에서.
코드가 다른 사람에 의해 OP에 제시되었고 모든 코드 C2에 버그가 없기 때문에 이미 이것에 대한 버그 보고서가있을 수 있습니다 . 이 분석이 다른 사람에게 유익한 정보가되기를 바랍니다.
유능한 아파 긴이 주석에서 지적했듯이, 이것은 최근의 버그 입니다. 모든 관심 있고 도움이되는 사람들에게 많은 의무가 있습니다. 🙂
답변
이 코드는 기술적으로 출력되지 않아야하기 때문에 솔직히 꽤 이상합니다 …
int i = 8;
while ((i -= 3) > 0);
… 항상 발생한다 i
인 -1
(8 – 6 = 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);
}
}
좋은 코딩 연습의 두 가지 점 …
- 오히려 사용
String.valueOf()
- 일부 코딩 표준은 문자열 리터럴
.equals()
이 인수가 아닌 의 대상이어야 하므로 NullPointerException을 최소화 하도록 지정합니다 .
이 문제가 발생하지 않는 유일한 방법은 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 10 진수 값 47)i
=-2
, 표시되는 문자는.
(ASCII 10 진수 값 46)i
=-3
, 표시되는 문자는-
(ASCII 10 진수 값 45)i
=-4
, 표시되는 문자는,
(ASCII 10 진수 값 44)i
=-5
, 표시되는 문자는+
(ASCII 10 진수 값 43)i
=-6
, 표시되는 문자는*
(ASCII 10 진수 값 42)i
=-7
, 표시되는 문자는)
(ASCII 10 진수 값 41)i
=-8
, 표시되는 문자는(
(ASCII 10 진수 값 40)입니다.i
=-9
, 표시되는 문자는'
(ASCII 10 진수 값 39)
실제로 흥미로운 것은 ASCII 십진수 48의 문자가 값 0
이고 48-1 = 47 (문자 /
) 등입니다.
답변
Java가 왜 임의의 출력을 제공하는지 알지 못하지만 루프 i
내부의 더 큰 값에 대해서는 연결에 문제가 있습니다 for
.
당신은 교체하는 경우 String value = i + "";
와 라인을String value = String.valueOf(i) ;
코드가 작동 예상대로.
+
int를 문자열로 변환하는 데 사용 하는 연결 은 기본이며 버그가있을 수 있습니다 (이상하게도 현재 발견하고 있음).
참고 : for 루프 내부의 i 값을 10000으로 줄였고 +
연결 문제에 직면하지 않았습니다 .
이 문제는 Java 이해 관계자에게보고해야하며 이에 대한 의견을 제시 할 수 있습니다.
편집 for 루프의 i 값을 3 백만으로 업데이트하고 다음과 같이 새로운 오류 세트를 보았습니다.
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입니다.