摘要:某個測試服務器試圖通過反射來修改變量的值,出現了時靈時不靈的現象。這個閾值隨時會變,只是測著玩的編譯是可以取消的,現在修改如下,在用反射設值后,再次執行萬次直接取值現在的執行結果又是了。結論不要修改變量,會出問題的關于編譯期優化的更多知識
某個測試服務器試圖通過反射來修改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://specialneedsforspecialkids.com/yun/67623.html
摘要:拆解虛擬機的基本步聚如下首先,要等待到自身成為唯一一個正在運行的非守護線程時,在整個等待過程中,虛擬機仍舊是可工作的。將相應的事件發送給,禁用,并終止信號線程。 本文簡單介紹HotSpot虛擬機運行時子系統,內容來自不同的版本,因此可能會與最新版本之間(當前為JDK12)存在一些誤差。 1.命令行參數處理HotSpot虛擬機中有大量的可影響性能的命令行屬性,可根據他們的消費者進行簡...
摘要:四后記理解好對象不僅能讓我們更好的認識一切皆對象這個觀點,對之后學習泛型,類型擦除都是很有幫助的,而對于反射機制我們只需在適當的場合利用它即可。 一 前言 很多書上都說,在java的世界里,一切皆對象。其實從某種意義上說,在java中有兩種對象:實例對象和Class對象。實例對象就是我們平常定義的一個類的實例: /** * Created by aristark on 3/28/16...
摘要:語言通過字節碼的方式,在一定程度上解決了傳統解釋型語言執行效率低的問題,同時又保留了解釋型語言可移植的特點。有針對不同系統的特定實現,,,目的是使用相同的字節碼,它們都會給出相同的結果。 showImg(https://segmentfault.com/img/bVbsjCK?w=800&h=450); 一、面向對象和面向過程的區別 面向過程優點: 性能比面向對象高,因為類調用時需要實...
摘要:語言通過字節碼的方式,在一定程度上解決了傳統解釋型語言執行效率低的問題,同時又保留了解釋型語言可移植的特點。有針對不同系統的特定實現,,,目的是使用相同的字節碼,它們都會給出相同的結果。項目主要基于捐贈的源代碼。 本文來自于我的慕課網手記:Java編程中那些再熟悉不過的知識點,轉載請保留鏈接 ;) 1. 面向對象和面向過程的區別 面向過程 優點: 性能比面向對象高。因為類調用時需要實例...
閱讀 482·2019-08-30 15:44
閱讀 897·2019-08-30 10:55
閱讀 2729·2019-08-29 15:16
閱讀 924·2019-08-29 13:17
閱讀 2801·2019-08-26 13:27
閱讀 568·2019-08-26 11:53
閱讀 2119·2019-08-23 18:31
閱讀 1882·2019-08-23 18:23