Esto se puede reproducir de manera confiable (o no, según lo que desee) con openjdk version "1.8.0_222"
(utilizado en mi análisis), OpenJDK 12.0.1
(según Oleksandr Pyrohov) y OpenJDK 13 (según Carlos Heuberger).
Ejecuté el código con -XX:+PrintCompilation
suficientes tiempos para obtener ambos comportamientos y aquí están las diferencias.
Implementación con errores (muestra la salida):
--- 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
Ejecución correcta (sin visualización):
--- 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 una diferencia significativa. Con la ejecución correcta compilamos test()
dos veces. Una vez al principio, y una vez más después (presumiblemente porque el JIT nota cuán caliente es el método). En el buggy la ejecución test()
se compila (o descompila) 5 veces.
Además, al ejecutarse con -XX:-TieredCompilation
(que interpreta o utiliza C2
) o con -Xbatch
(que obliga a la compilación a ejecutarse en el hilo principal, en lugar de en paralelo), se garantiza la salida y con 30000 iteraciones imprime muchas cosas, por lo que el C2
compilador parece ser el culpable Esto se confirma al ejecutar with -XX:TieredStopAtLevel=1
, que deshabilita C2
y no produce salida (detenerse en el nivel 4 muestra el error nuevamente).
En la ejecución correcta, el método se compila primero con la compilación de Nivel 3 y luego con el Nivel 4.
En la ejecución con errores, se descartan las compilaciones anteriores (made non entrant
) y se vuelve a compilar en el Nivel 3 (es decir C1
, ver enlace anterior).
Entonces definitivamente es un error en C2
, aunque no estoy absolutamente seguro de si el hecho de que esté volviendo a la compilación del Nivel 3 lo afecta (y por qué está volviendo al nivel 3, todavía hay muchas incertidumbres).
Puede generar el código de ensamblaje con la siguiente línea para profundizar aún más en el agujero del conejo (también vea esto para habilitar la impresión de ensamblaje).
java -XX:+PrintCompilation -Xbatch -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly LoopOutPut > broken.asm
En este punto, estoy empezando a quedarme sin habilidades, el comportamiento con errores comienza a exhibirse cuando se descartan las versiones compiladas anteriores, pero las pocas habilidades de ensamblaje que tengo son de los 90, así que dejaré que alguien más inteligente que yo lo tome de aquí.
Es probable que ya haya un informe de error sobre esto, ya que el código fue presentado al OP por otra persona, y como todo el código C2 no está exento de errores . Espero que este análisis haya sido tan informativo para otros como lo ha sido para mí.
Como el venerable apangin señaló en los comentarios, este es un error reciente . Muy agradecido con todas las personas interesadas y serviciales :)