某个测试服务器试图通过反射来修改static final变量的值,出现了时灵时不灵的现象。
开发环境无法重现。这是怎么回事呢?
先介绍背景知识
一般认为,static final常量会被编译器执行内联优化,即它的值会被内联到调用位置。
这对于如下方式初始化的字面常量有效:
private static final boolean MY_VALUE = false;
但对于如下方式初始化的运行时常量无效:
private static final boolean MY_VALUE = System.getProperty("dsasdkdfskdsdfk") != null;
为什么会不一样呢?因为第一种方式字面量(literal, 硬编码在代码里的值,可以是布尔值、数值、字符串等等)是编译时就能确定的,而第二种方式的值是某个调用的返回值,直到运行的那一刻才确定。
具体的常量优化规则可参考语言规范:http://docs.oracle.com/javase...
然后我就发现一个危险现象:引用自另一个jar的常量也会被内联!
如果你引用一个第三方库中的常量,然后升级了这个库的版本,新版本改变了常量的值,那么你的程序就错了!除非你重新编译你的程序!
有时候这是很隐蔽的!例如你引用的是Tomcat的一个常量,然后你直接把程序放在新版本的Tomcat中运行!
然后解决当前的问题
服务器上的问题是:用反射强行修改static final变量的值,用反射能取得修改后的值,然而Java调用直接取得的值却仍是旧值。
可用如下Test.java MyEnv.java两个文件来重现,但是在开发环境并没有重现出问题:
Test.java
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
public class Test {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Field myField = MyEnv.class.getDeclaredField("MY_VALUE");
myField.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(myField, myField.getModifiers() & ~Modifier.FINAL);
myField.set(null, true);
System.out.println("Get via reflection: " + myField.get(null)); // true on the server
System.out.println("Get directly:" + MyEnv.getValue()); // false on the server
}
}
MyEnv.java
public class MyEnv {
private static final boolean MY_VALUE = System.getProperty("dsasdkdfskdsdfk") != null;
public static boolean getValue() {
return MY_VALUE;
}
}
按照语言规范里的编译器常量优化规则,这个常量不会被内联,所以开发环境的执行结果(两个都是true)似乎是对的?
但是JVM有运行时优化——当代码频繁执行时,会触发JIT编译!
我们修改Test.java如下,执行了10万次直接取值:
Test.java
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
public class Test {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
for (int i = 0; i < 100000; i++) {
MyEnv.getValue();
}
Field myField = MyEnv.class.getDeclaredField("MY_VALUE");
myField.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(myField, myField.getModifiers() & ~Modifier.FINAL);
myField.set(null, true);
System.out.println("Get via reflection: " + myField.get(null)); // true on the server
System.out.println("Get directly:" + MyEnv.getValue()); // false on the server
}
}
现在的执行结果是true, false,重现了服务器的问题。原因是JVM在运行时通过JIT编译再次内联了常量。
在我的电脑上,触发这个JIT编译的阈值是15239,远小于10万。(这个阈值随时会变,只是测着玩的)
JIT编译是可以取消的,现在修改Test.java如下,在用反射设值后,再次执行10万次直接取值:
public class Test {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
for (int i = 0; i < 100000; i++) {
MyEnv.getValue();
}
Field myField = MyEnv.class.getDeclaredField("MY_VALUE");
myField.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(myField, myField.getModifiers() & ~Modifier.FINAL);
myField.set(null, true);
for (int i = 0; i < 100000; i++) {
MyEnv.getValue();
}
System.out.println("Get via reflection: " + myField.get(null)); // true on the server
System.out.println("Get directly:" + MyEnv.getValue()); // false on the server
}
}
现在的执行结果又是true, true了。
与其说是取消了JIT,不如说是触发了新一次JIT!可以用代码验证这一推测,这个就留作思考题了:)
(注意,要想触发新的JIT,需要更大量的执行次数。)
结论:不要修改final变量,会出问题的!
关于编译期优化的更多知识 https://briangordon.github.io...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。