某個測試服務器試圖經過反射來修改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...