Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Breaking JIT optimisations with reflection

When fiddling with unit tests for a highly-concurrent singleton class I stumbled upon the following weird behaviour (tested on JDK 1.8.0_162):

private static class SingletonClass {
    static final SingletonClass INSTANCE = new SingletonClass(0);
    final int value;

    static SingletonClass getInstance() {
        return INSTANCE;
    }

    SingletonClass(int value) {
        this.value = value;
    }
}

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {

    System.out.println(SingletonClass.getInstance().value); // 0

    // Change the instance to a new one with value 1
    setSingletonInstance(new SingletonClass(1));
    System.out.println(SingletonClass.getInstance().value); // 1

    // Call getInstance() enough times to trigger JIT optimizations
    for(int i=0;i<100_000;++i){
        SingletonClass.getInstance();
    }

    System.out.println(SingletonClass.getInstance().value); // 1

    setSingletonInstance(new SingletonClass(2));
    System.out.println(SingletonClass.INSTANCE.value); // 2
    System.out.println(SingletonClass.getInstance().value); // 1 (2 expected)
}

private static void setSingletonInstance(SingletonClass newInstance) throws NoSuchFieldException, IllegalAccessException {
    // Get the INSTANCE field and make it accessible
    Field field = SingletonClass.class.getDeclaredField("INSTANCE");
    field.setAccessible(true);

    // Remove the final modifier
    Field modifiersField = Field.class.getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

    // Set new value
    field.set(null, newInstance);
}

The last 2 lines of the main() method disagree on the value of INSTANCE - my guess is that JIT got rid of the method completely since the field is static final. Removing the final keyword makes the code output correct values.

Leaving aside your sympathy (or lack thereof) for singletons and forgetting for a minute that using reflection like this is asking for trouble - is my assumption correct in that JIT optimisations are to blame? If so - are those limited to static final fields only?

like image 397
Kelm Avatar asked Jan 07 '20 17:01

Kelm


1 Answers

Taking your question literally, “…is my assumption correct in that JIT optimizations are to blame?”, the answer is yes, it’s very likely that the JIT optimizations are responsible for this behavior in this specific example.

But since changing static final fields is completely off specification, there are other things that can break it similarly. E.g. the JMM has no definition for the memory visibility of such changes, hence, it is completely unspecified whether or when other threads notice such changes. They are not even required to notice it consistently, i.e. they may use the new value, followed by using the old value again, even in the presence of synchronization primitives.

Though, the JMM and the optimizer are hard to separate anyway here.

Your question “…are those limited to static final fields only?” is much harder to answer, as optimizations are, of course, not limited to static final fields, but the behavior of, e.g. non-static final fields, is is not the same and has differences between theory and practice as well.

For non-static final fields, modifications via Reflection are allowed in certain circumstances. This is indicated by the fact that setAccessible(true) is enough to make such modification possible, without hacking into the Field instance to change the internal modifiers field.

The specification says:

17.5.3. Subsequent Modification of final Fields

In some cases, such as deserialization, the system will need to change the final fields of an object after construction. final fields can be changed via reflection and other implementation-dependent means. The only pattern in which this has reasonable semantics is one in which an object is constructed and then the final fields of the object are updated. The object should not be made visible to other threads, nor should the final fields be read, until all updates to the final fields of the object are complete. Freezes of a final field occur both at the end of the constructor in which the final field is set, and immediately after each modification of a final field via reflection or other special mechanism.

Another problem is that the specification allows aggressive optimization of final fields. Within a thread, it is permissible to reorder reads of a final field with those modifications of a final field that do not take place in the constructor.

Example 17.5.3-1. Aggressive Optimization of final Fields
class A {
    final int x;
    A() { 
        x = 1; 
    } 

    int f() { 
        return d(this,this); 
    } 

    int d(A a1, A a2) { 
        int i = a1.x; 
        g(a1); 
        int j = a2.x; 
        return j - i; 
    }

    static void g(A a) { 
        // uses reflection to change a.x to 2 
    } 
}

In the d method, the compiler is allowed to reorder the reads of x and the call to g freely. Thus, new A().f() could return -1, 0, or 1.

In practice, determining the right places where aggressive optimizations are possible without breaking the legal scenarios described above, are an open issue, so unless -XX:+TrustFinalNonStaticFields has been specified, the HotSpot JVM will not optimize non-static final fields the same way as static final fields.

Of course, when you don’t declare the field as final, the JIT can not assume that it will never change, though, in the absence of thread synchronization primitives, it may consider the actual modifications happening in the code path it optimizes (including the reflective ones). So it may still aggressively optimize the access, but only as-if the reads and writes still happen in the program order within the executing thread. So you’d only notice the optimizations when looking at it from a different thread without proper synchronization constructs.

like image 193
Holger Avatar answered Nov 04 '22 05:11

Holger