Non mi ero mai imbattuto in un bug della Java Virtual Machine fino alla scorsa estate. Stavo collaudando l’applicazione RouterLogger, la quale basa il suo funzionamento su un ciclo di richieste di rete che può essere infinito oppure limitato ad un numero di iterazioni configurabile. La prima implementazione prevedeva un ciclo quasi infinito:
private void loop() throws IOException, InterruptedException { /* Determinazione numero di iterazioni... */ int iterations = configuration.getInt("logger.iterations", -1); if (iterations <= 0) { iterations = Integer.MAX_VALUE; } for (int iteration = 1; iteration <= iterations && !exit; iteration++) { /* Richiesta di rete... */ if (iteration != iterations) { /* Thread.sleep(waitTimeInMillis); */ } } }
Nel caso in cui la proprietà di configurazione logger.iterations
fosse stata impostata ad un valore negativo (tipicamente -1
), il programma avrebbe impostato come numero di iterazioni da eseguire Integer.MAX_VALUE
(ossia 2.147.483.647
), determinando un ciclo praticamente infinito considerando che, nel caso di RouterLogger, un’iterazione dura normalmente qualche secondo.
Ebbene, eseguendo un test con logger.iterations=-1
, notavo che il loop terminava dopo un certo numero di iterazioni (intorno a sedicimila), sempre lo stesso, e senza alcun errore o eccezione, come se la condizione di permanenza nel ciclo iteration <= iterations && !exit
non fosse più verificata, cosa evidentemente falsa. Controllato il codice e verificatane la correttezza, iniziavo a non capire cosa stesse succedendo. La confusione è aumentata quando ho notato che facendo girare l’applicazione direttamente da Eclipse, quindi utilizzando il compilatore integrato in Eclipse (JDT) invece di quello del JDK 6u45, il problema non si presentava affatto. Decidevo quindi fare il reverse engineering dei due compilati (.class
) per scongiurare un eventuale difetto del compilatore del JDK, ma anche quest’indagine non portava da nessuna parte: i due bytecode, pur essendo differenti, non presentavano anomalie. Tanto per curiosità, pubblico il bytecode generato prima con il JDK e poi con Eclipse, da me reinterpretato in C per maggiore chiarezza:
JDK 6u45:
14: int iterations = 2147483647; 20: int iteration = 0; 22: if (iteration > iterations) goto 152; 28: if (exit) goto 152; 35: /* Richiesta di rete... */ 62: if (iteration == iterations) goto 146; 143: /* Thread.sleep(waitTimeInMillis); */ 146: iteration++; 149: goto 22; 152: return;
Eclipse JDT:
18: int iterations = 2147483647; 22: int iteration = 0; 24: goto 144; 27: /* Richiesta di rete... */ 54: if (iteration == iterations) goto 141; 138: /* Thread.sleep(waitTimeInMillis); */ 141: iteration++; 144: if (iteration > iterations) goto 157; 154: if (!exit) goto 27; 157: return;
Seppur diversi, entrambi i codici sono logicamente corretti. Escluso un problema legato al compilatore, resta quasi solo l’ipotesi di un bug della JVM. Effettivamente aggiornando il JRE alla versione 7u80, il problema scompariva del tutto, indipendentemente dal compilatore utilizzato, tuttavia mi faceva piacere mantenere la compatibilità con il JRE 6. Inoltre la cosa è inquietante perché il codice è di una semplicità unica e il comportamento della JVM risulta totalmente illogico.
Dopo alcune ricerche, ho individuato che esiste effettivamente un bug della JVM (JDK-5091921) legato ad un’ottimizzazione che viene eseguita a runtime. In pratica, a un certo momento non ben precisato durante l’esecuzione del programma, la JVM decide di ottimizzare il loop modificando la condizione di permanenza nel seguente modo: i <= j
diventa i < j + 1
, probabilmente perché per il processore è più semplice eseguire un controllo di minoranza stretta; la cosa non sarebbe un problema se non fosse che, nel caso j = Integer.MAX_VALUE
, la somma Integer.MAX_VALUE + 1
, a causa di un overflow, risulti in un bel numerone negativo (per la precisione Integer.MIN_VALUE
ossia -2.147.483.648
). Per questo motivo l’indice dell’iterazione, qualsiasi esso fosse, diventava immediatamente maggiore di -2.147.483.648, e il loop terminava come se niente fosse e senza alcun errore.
Per rendere il mio codice compatibile con il JRE 6, ho deciso di modificarlo rimuovendo il riferimento ad Integer.MAX_VALUE
, pervenendo tra l’altro ad una soluzione più semplice e pulita:
private void loop() throws IOException, InterruptedException { /* Determinazione numero di iterazioni... */ int iterations = configuration.getInt("logger.iterations", -1); for (int iteration = 1; (iterations <= 0 || iteration <= iterations) && !exit; iteration++) { /* Richiesta di rete... */ if (iteration != iterations) { /* Thread.sleep(waitTimeInMillis); */ } } }
Questo bug, scoperto nel 2004 e risegnalato numerose volte alla Sun (acquisita nel frattempo da Oracle nel 2010), era stato classificato a bassa priorità (no comment), ed è stato risolto solo nel 2011 con l’aggiunta di un controllo sull’overflow. In ogni caso, per maggiore sicurezza, conviene prestare particolare attenzione quando si ha a che fare con i MAX_VALUE
.