Isso pode ser reproduzido de maneira confiável (ou não, dependendo do que você deseja) com openjdk version "1.8.0_222"
(usado em minha análise), OpenJDK 12.0.1
(de acordo com Oleksandr Pyrohov) e OpenJDK 13 (de acordo com Carlos Heuberger).
Corri o código com -XX:+PrintCompilation
tempo suficiente para obter os dois comportamentos e aqui estão as diferenças.
Implementação de buggy (exibe saída):
--- 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
Execução correta (sem exibição):
--- 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)
Podemos notar uma diferença significativa. Com a execução correta, compilamos test()
duas vezes. Uma vez no início e mais uma vez depois (presumivelmente porque o JIT percebe o quão quente é o método). No buggy, a execução test()
é compilada (ou descompilada) 5 vezes.
Além disso, executando com -XX:-TieredCompilation
(que interpreta ou usa C2
) ou com -Xbatch
(que força a compilação a executar no encadeamento principal, em vez de paralelamente), a saída é garantida e, com 30000 iterações, imprime muitas coisas, portanto o C2
compilador parece ser o culpado. Isso é confirmado com a execução de -XX:TieredStopAtLevel=1
, que desativa C2
e não produz saída (parar no nível 4 mostra o bug novamente).
Na execução correta, o método é compilado primeiro com a compilação do Nível 3 e depois com o Nível 4.
Na execução do buggy, as compilações anteriores são descartadas ( made non entrant
) e são compiladas novamente no nível 3 (ou seja C1
, consulte o link anterior).
Definitivamente, é um bug C2
, embora eu não tenha certeza absoluta de que o fato de voltar à compilação do nível 3 a afeta (e por que está voltando ao nível 3, ainda há muitas incertezas).
Você pode gerar o código de montagem com a seguinte linha para ir ainda mais fundo na toca do coelho (veja também isso para ativar a impressão de montagem).
java -XX:+PrintCompilation -Xbatch -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly LoopOutPut > broken.asm
Neste ponto, estou começando a ficar sem habilidades, o comportamento do buggy começa a ser exibido quando as versões compiladas anteriores são descartadas, mas que poucas habilidades de montagem eu tenho desde os anos 90, então deixarei alguém mais esperto do que eu usá-lo daqui.
É provável que já exista um relatório de bug sobre isso, uma vez que o código foi apresentado ao OP por outra pessoa e, como todo o código C2, não há erros . Espero que essa análise tenha sido tão informativa para os outros quanto para mim.
Como o venerável apangin apontou nos comentários, este é um bug recente . Muito obrigado a todas as pessoas interessadas e prestativas :)