某个测试服务器试图经过反射来修改static final变量的值,出现了时灵时不灵的现象。html
开发环境没法重现。这是怎么回事呢?java
通常认为,static final常量会被编译器执行内联优化,即它的值会被内联到调用位置。git
这对于以下方式初始化的字面常量有效:github
private static final boolean MY_VALUE = false;
但对于以下方式初始化的运行时常量无效:服务器
private static final boolean MY_VALUE = System.getProperty("dsasdkdfskdsdfk") != null;
为何会不同呢?由于第一种方式字面量(literal, 硬编码在代码里的值,能够是布尔值、数值、字符串等等)是编译时就能肯定的,而第二种方式的值是某个调用的返回值,直到运行的那一刻才肯定。oracle
具体的常量优化规则可参考语言规范:http://docs.oracle.com/javase...测试
而后我就发现一个危险现象:引用自另外一个jar的常量也会被内联!优化
若是你引用一个第三方库中的常量,而后升级了这个库的版本,新版本改变了常量的值,那么你的程序就错了!除非你从新编译你的程序!编码
有时候这是很隐蔽的!例如你引用的是Tomcat的一个常量,而后你直接把程序放在新版本的Tomcat中运行!code
服务器上的问题是:用反射强行修改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...