Cela peut être reproduit de manière fiable (ou non reproduit, selon ce que vous voulez) avec openjdk version "1.8.0_222"
(utilisé dans mon analyse), OpenJDK 12.0.1
(selon Oleksandr Pyrohov) et OpenJDK 13 (selon Carlos Heuberger).
J'ai exécuté le code avec -XX:+PrintCompilation
suffisamment de temps pour obtenir les deux comportements et voici les différences.
Implémentation du buggy (affiche la sortie):
--- 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
Exécution correcte (pas d'affichage):
--- 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)
Nous pouvons remarquer une différence significative. Avec la bonne exécution, nous compilons test()
deux fois. Une fois au début, et encore une fois après (probablement parce que le JIT remarque à quel point la méthode est chaude). Dans le buggy, l'exécution test()
est compilée (ou décompilée) 5 fois.
De plus, en exécutant avec -XX:-TieredCompilation
(qui interprète ou utilise C2
) ou avec -Xbatch
(qui force la compilation à s'exécuter dans le thread principal, au lieu de parallèlement), la sortie est garantie et avec 30000 itérations imprime beaucoup de choses, donc le C2
compilateur semble être le coupable. Ceci est confirmé par l'exécution de -XX:TieredStopAtLevel=1
, qui désactive C2
et ne produit pas de sortie (l'arrêt au niveau 4 montre à nouveau le bogue).
Dans l'exécution correcte, la méthode est d'abord compilée avec la compilation de niveau 3 , puis avec le niveau 4.
Dans l'exécution du buggy, les compilations précédentes sont supprimées ( made non entrant
) et elles sont à nouveau compilées au niveau 3 (c'est-à-dire C1
, voir le lien précédent).
Il s'agit donc définitivement d'un bug C2
, même si je ne suis pas absolument sûr de savoir si le fait de revenir à la compilation de niveau 3 l'affecte (et pourquoi revient-il au niveau 3, tant d'incertitudes encore).
Vous pouvez générer le code d'assemblage avec la ligne suivante pour aller encore plus loin dans le trou du lapin (voir également ceci pour activer l'impression d'assemblage).
java -XX:+PrintCompilation -Xbatch -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly LoopOutPut > broken.asm
À ce stade, je commence à manquer de compétences, le comportement du buggy commence à se manifester lorsque les versions compilées précédentes sont supprimées, mais le peu de compétences d'assemblage que j'ai des années 90, donc je vais laisser quelqu'un plus intelligent que moi le prendre d'ici.
Il est probable qu'il existe déjà un rapport de bogue à ce sujet, car le code a été présenté à l'OP par quelqu'un d'autre, et comme tout le code C2 n'est pas sans bogues . J'espère que cette analyse a été aussi informative pour les autres que pour moi.
Comme l'a souligné le vénérable apangin dans les commentaires, il s'agit d'un bug récent . Je suis très obligé envers toutes les personnes intéressées et utiles :)