I have been faced in some of my Unit test with a strange behaviour with Reflection on final static field. Below is an example illustrating my issue.
I have a basic Singleton class that holds an Integer
public class BasicHolder {
private static BasicHolder instance = new BasicHolder();
public static BasicHolder getInstance() {
return instance;
}
private BasicHolder() {
}
private final static Integer VALUE = new Integer(0);
public Integer getVALUE() {
return VALUE;
}
}
My test case is looping and setting through Reflection the VALUE to the iteration index and then asserting that the VALUE is rightfully equal to the iteration index.
class TestStaticLimits {
private static final Integer NB_ITERATION = 10_000;
@Test
void testStaticLimit() {
for (Integer i = 0; i < NB_ITERATION; i++) {
setStaticFieldValue(BasicHolder.class, "VALUE", i);
Assertions.assertEquals(i, BasicHolder.getInstance().getVALUE(), "REFLECTION DID NOT WORK for iteration "+i);
System.out.println("iter " + i + " ok" );
}
}
private static void setStaticFieldValue(final Class obj, final String fieldName, final Object fieldValue) {
try {
final Field field = obj.getDeclaredField(fieldName);
field.setAccessible(true);
final Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, fieldValue);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("Error while setting field [" + fieldName + "] on object " + obj + " Message " + e.getMessage(), e);
}
}
}
The result is quite surprising because it's not constant, my test fails around iteration ~1000 but it never seems to be always the same.
Anyone has already faced this issue ?
It's because of the JIT optimization. To prove this, disable it using the following
VM
option:In this case all
10_000
iterations will work.Or, exclude the
BasicHolder.getVALUE
method from being compiled:What actually happens under the hood is that after
nth
iteration, the hot methodgetVALUE
is being compiled andstatic final Integer VALUE
is being aggressively optimized (this is really the just-in-time constant1). From this point, the assertion starts to fail.The output of the
-XX:+PrintCompilation
with my comments:1 - JVM Anatomy Park: Just-In-Time Constants.
The JLS mentions that modifying final fields after construction is problematic - see 17.5. final Field Semantics
and 17.5.3. Subsequent Modification of final Fields:
In addition to that, the JavaDocs of Field.set also include a warning about this:
It seems that what we are witnessing here is the JIT taking advantage of the reordering and caching possibilities granted by the Language Specification.