Można to rzetelnie odtworzyć (lub nie odtworzyć, w zależności od tego, czego chcesz) za pomocą openjdk version "1.8.0_222"
(zastosowanego w mojej analizie), OpenJDK 12.0.1
(według Oleksandra Pyrohova) i OpenJDK 13 (według Carlosa Heubergera).
Uruchomiłem kod z -XX:+PrintCompilation
wystarczającą ilością razy, aby uzyskać oba zachowania, a oto różnice.
Implementacja błędna (wyświetla dane wyjściowe):
--- 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
Prawidłowy przebieg (bez wskazania):
--- 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)
Możemy zauważyć jedną istotną różnicę. Przy poprawnym wykonaniu kompilujemy test()
dwa razy. Raz na początku i jeszcze raz później (prawdopodobnie dlatego, że JIT zauważa, jak gorąca jest ta metoda). W buggy wykonanie test()
jest kompilowane (lub dekompilowane) 5 razy.
Dodatkowo, uruchamianie z -XX:-TieredCompilation
(który interpretuje lub używa C2
) lub z -Xbatch
(co zmusza kompilację do uruchamiania w głównym wątku, zamiast równolegle), wyjście jest gwarantowane, a przy 30000 iteracjach drukuje wiele rzeczy, więc C2
kompilator wydaje się być winowajcą. Potwierdza to uruchomienie z -XX:TieredStopAtLevel=1
, które wyłącza C2
i nie generuje danych wyjściowych (zatrzymanie na poziomie 4 ponownie pokazuje błąd).
W prawidłowym wykonaniu metoda jest najpierw kompilowana z kompilacją poziomu 3 , a następnie z poziomem 4.
W wykonaniu buggy poprzednie kompilacje są ignorowane ( made non entrant
) i ponownie kompilowane na poziomie 3 (czyli C1
patrz poprzedni link).
Więc na pewno jest to błąd C2
, chociaż nie jestem absolutnie pewien, czy wpływa na to fakt, że wraca do kompilacji poziomu 3 (i dlaczego wraca do poziomu 3, wciąż jest tak wiele niepewności).
Można wygenerować kod montażowej z następującą linię iść nawet głębiej do króliczej nory (patrz również to , aby umożliwić montaż drukowania).
java -XX:+PrintCompilation -Xbatch -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly LoopOutPut > broken.asm
W tym momencie zaczynam brakować umiejętności, zachowanie buggy zaczyna się ujawniać, gdy poprzednie kompilowane wersje są odrzucane, ale to, co mam małe umiejętności montażowe, mam z lat 90-tych, więc pozwolę, by ktoś mądrzejszy ode mnie wziął stąd.
Prawdopodobnie jest już raport o błędzie na ten temat, ponieważ kod został przedstawiony OP przez kogoś innego, a ponieważ cały kod C2 nie jest bez błędów . Mam nadzieję, że ta analiza była tak samo pouczająca dla innych, jak dla mnie.
Jak zauważył czcigodny apangin w komentarzach, jest to ostatni błąd . Bardzo zobowiązany do wszystkich zainteresowanych i pomocnych osób :)