JVM JIT編譯能改變某些反射的執行結果

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

相關文章
相關標籤/搜索