用反射打破 JIT 优化
Breaking JIT optimisations with reflection
在对高并发单例进行单元测试时 class 我偶然发现了以下奇怪的行为(在 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);
}
main() 方法的最后两行不同意 INSTANCE 的值 - 我的猜测是 JIT 完全摆脱了该方法,因为该字段是静态最终的。删除 final 关键字使代码输出正确的值。
抛开您对单身人士的同情(或缺乏同情),暂时忘记像这样使用反射是自找麻烦 - 我的假设是否正确,因为 JIT 优化是罪魁祸首?如果是这样 - 那些仅限于静态最终字段吗?
从字面上理解你的问题,“…我的假设是否正确,因为 JIT 优化是罪魁祸首?”,答案是肯定的,很可能是 JIT 优化负责对于此特定示例中的此行为。
但是由于更改 static final
字段完全不符合规范,因此还有其他类似的事情可以破坏它。例如。 JMM 没有定义此类更改的内存可见性,因此,完全不确定其他线程是否或何时注意到此类更改。他们甚至不需要始终如一地注意到它,即他们可以使用新值,然后再次使用旧值,即使存在同步原语。
尽管如此,JMM 和优化器在这里还是很难分开。
你的问题“…那些仅限于静态最终字段吗?”更难回答,因为优化当然不限于 static final
领域,但行为,例如非静态 final
字段,是不一样的,理论和实践之间也有差异。
对于非静态 final
字段,在某些情况下允许通过反射进行修改。这表明 setAccessible(true)
足以使这种修改成为可能,而无需侵入 Field
实例来更改内部 modifiers
字段。
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
.
在实践中,在不破坏上述合法场景的情况下确定可以进行积极优化的正确位置是 an open issue,因此除非指定了 -XX:+TrustFinalNonStaticFields
,否则 HotSpot JVM 不会优化非-static final
字段与 static final
字段相同。
当然,当你不声明字段为final
时,JIT不能假设它永远不会改变,但是,在没有线程同步原语的情况下,它可能会考虑实际修改发生在它优化的代码路径中(包括反射路径)。因此它可能仍然会积极优化访问,但只有 as-if 读取和写入仍然在执行线程内按程序顺序发生。因此,只有在没有适当的同步构造的情况下从不同的线程查看它时,您才会注意到优化。
在对高并发单例进行单元测试时 class 我偶然发现了以下奇怪的行为(在 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);
}
main() 方法的最后两行不同意 INSTANCE 的值 - 我的猜测是 JIT 完全摆脱了该方法,因为该字段是静态最终的。删除 final 关键字使代码输出正确的值。
抛开您对单身人士的同情(或缺乏同情),暂时忘记像这样使用反射是自找麻烦 - 我的假设是否正确,因为 JIT 优化是罪魁祸首?如果是这样 - 那些仅限于静态最终字段吗?
从字面上理解你的问题,“…我的假设是否正确,因为 JIT 优化是罪魁祸首?”,答案是肯定的,很可能是 JIT 优化负责对于此特定示例中的此行为。
但是由于更改 static final
字段完全不符合规范,因此还有其他类似的事情可以破坏它。例如。 JMM 没有定义此类更改的内存可见性,因此,完全不确定其他线程是否或何时注意到此类更改。他们甚至不需要始终如一地注意到它,即他们可以使用新值,然后再次使用旧值,即使存在同步原语。
尽管如此,JMM 和优化器在这里还是很难分开。
你的问题“…那些仅限于静态最终字段吗?”更难回答,因为优化当然不限于 static final
领域,但行为,例如非静态 final
字段,是不一样的,理论和实践之间也有差异。
对于非静态 final
字段,在某些情况下允许通过反射进行修改。这表明 setAccessible(true)
足以使这种修改成为可能,而无需侵入 Field
实例来更改内部 modifiers
字段。
17.5.3. Subsequent Modification of
final
FieldsIn 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 thefinal
fields of the object are updated. The object should not be made visible to other threads, nor should thefinal
fields be read, until all updates to thefinal
fields of the object are complete. Freezes of afinal
field occur both at the end of the constructor in which thefinal
field is set, and immediately after each modification of afinal
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 afinal
field with those modifications of afinal
field that do not take place in the constructor.Example 17.5.3-1. Aggressive Optimization offinal
Fieldsclass 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 ofx
and the call tog
freely. Thus,new A().f()
could return-1
,0
, or1
.
在实践中,在不破坏上述合法场景的情况下确定可以进行积极优化的正确位置是 an open issue,因此除非指定了 -XX:+TrustFinalNonStaticFields
,否则 HotSpot JVM 不会优化非-static final
字段与 static final
字段相同。
当然,当你不声明字段为final
时,JIT不能假设它永远不会改变,但是,在没有线程同步原语的情况下,它可能会考虑实际修改发生在它优化的代码路径中(包括反射路径)。因此它可能仍然会积极优化访问,但只有 as-if 读取和写入仍然在执行线程内按程序顺序发生。因此,只有在没有适当的同步构造的情况下从不同的线程查看它时,您才会注意到优化。