摘要:某个测试服务器试图通过反射来修改变量的值,出现了时灵时不灵的现象。这个阈值随时会变,只是测着玩的编译是可以取消的,现在修改如下,在用反射设值后,再次执行万次直接取值现在的执行结果又是了。结论不要修改变量,会出问题的关于编译期优化的更多知识
某个测试服务器试图通过反射来修改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...
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/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. 面向对象和面向过程的区别 面向过程 优点: 性能比面向对象高。因为类调用时需要实例...
阅读 466·2019-08-30 15:44
阅读 878·2019-08-30 10:55
阅读 2715·2019-08-29 15:16
阅读 885·2019-08-29 13:17
阅读 2783·2019-08-26 13:27
阅读 537·2019-08-26 11:53
阅读 2094·2019-08-23 18:31
阅读 1871·2019-08-23 18:23