用反射打破 JIT 优化

Kel*_*elm 8 java reflection jit java-8

在为高度并发的单例类进行单元测试时,我偶然发现了以下奇怪的行为(在 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);
}
Run Code Online (Sandbox Code Playgroud)

main() 方法的最后两行不同意 INSTANCE 的值 - 我的猜测是 JIT 完全摆脱了该方法,因为该字段是静态最终的。删除 final 关键字使代码输出正确的值。

抛开你对单身人士的同情(或缺乏同情),一分钟忘记使用这样的反射是在自找麻烦——我的假设是否正确,因为 JIT 优化是罪魁祸首?如果是这样 - 那些仅限于静态最终字段吗?

Hol*_*ger 6

从字面上理解你的问题,“ ……我的假设是否正确,因为 JIT 优化是罪魁祸首?”,答案是肯定的,在此特定示例中,JIT 优化很可能对这种行为负责。

但是由于更改static final字段完全不符合规范,因此还有其他事情可以类似地破坏它。例如,JMM 没有定义此类更改的内存可见性,因此,完全不确定其他线程是否或何时注意到此类更改。他们甚至不需要始终如一地注意到它,即他们可以使用新值,然后再次使用旧值,即使存在同步原语。

尽管如此,JMM 和优化器在这里无论如何都很难分开。

您的问题“ ……那些仅限于静态最终字段吗?” 更难回答,因为优化当然不仅限于static final字段,但非静态final字段的行为并不相同,并且在理论和实践之间也存在差异。

对于非静态final字段,在某些情况下允许通过反射进行修改。事实表明,这setAccessible(true)足以使此类修改成为可能,而无需侵入Field实例以更改内部modifiers字段。

规范说:

17.5.3. final字段的后续修改

在某些情况下,例如反序列化,系统将需要final在构造后更改对象的字段。final字段可以通过反射和其他依赖于实现的方式来改变。具有合理语义的唯一模式是构造一个对象然后final更新对象的字段。在对象final字段的所有更新final完成之前,不应使对象对其他线程可见,也不应读取字段。final字段的冻结发生在final设置该字段的构造函数的末尾,以及在每次final通过反射或其他特殊机制修改字段后立即发生。

另一个问题是规范允许对final字段进行积极的优化。在一个线程中,允许对一个final字段的读取重新排序,而这些修改final在构造函数中没有发生。

例 17.5.3-1。final字段的积极优化
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 
    } 
}
Run Code Online (Sandbox Code Playgroud)

在该d方法中,允许编译器自由地对 的读取x和调用重新排序g。因此,new A().f()可以返回-10、 或1

实际上,在不违反上述合法场景的情况下确定可以进行积极优化的正确位置是一个悬而未决的问题,因此除非-XX:+TrustFinalNonStaticFields已指定,否则HotSpot JVM 不会final以与字段相同的方式优化非静态static final字段。

当然,当您不将字段声明为 时final,JIT 不能假设它永远不会改变,但是,在没有线程同步原语的情况下,它可能会考虑在它优化的代码路径中发生的实际修改(包括反光的)。因此,它可能仍然积极优化访问,但只作为假设的读取和写入操作仍然在执行线程内的程序顺序发生。因此,只有在没有适当同步构造的情况下从不同线程查看它时,您才会注意到优化。