资讯专栏INFORMATION COLUMN

JVM JIT编译能改变某些反射的执行结果

lcodecorex / 1754人阅读

摘要:某个测试服务器试图通过反射来修改变量的值,出现了时灵时不灵的现象。这个阈值随时会变,只是测着玩的编译是可以取消的,现在修改如下,在用反射设值后,再次执行万次直接取值现在的执行结果又是了。结论不要修改变量,会出问题的关于编译期优化的更多知识

某个测试服务器试图通过反射来修改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

相关文章

  • JAVA运行时简述(HotSpot)

    摘要:拆解虚拟机的基本步聚如下首先,要等待到自身成为唯一一个正在运行的非守护线程时,在整个等待过程中,虚拟机仍旧是可工作的。将相应的事件发送给,禁用,并终止信号线程。 本文简单介绍HotSpot虚拟机运行时子系统,内容来自不同的版本,因此可能会与最新版本之间(当前为JDK12)存在一些误差。 1.命令行参数处理HotSpot虚拟机中有大量的可影响性能的命令行属性,可根据他们的消费者进行简...

    hosition 评论0 收藏0
  • Class对象和Java反射机制

    摘要:四后记理解好对象不仅能让我们更好的认识一切皆对象这个观点,对之后学习泛型,类型擦除都是很有帮助的,而对于反射机制我们只需在适当的场合利用它即可。 一 前言 很多书上都说,在java的世界里,一切皆对象。其实从某种意义上说,在java中有两种对象:实例对象和Class对象。实例对象就是我们平常定义的一个类的实例: /** * Created by aristark on 3/28/16...

    Rainie 评论0 收藏0
  • Class对象和Java反射机制

    摘要:四后记理解好对象不仅能让我们更好的认识一切皆对象这个观点,对之后学习泛型,类型擦除都是很有帮助的,而对于反射机制我们只需在适当的场合利用它即可。 一 前言 很多书上都说,在java的世界里,一切皆对象。其实从某种意义上说,在java中有两种对象:实例对象和Class对象。实例对象就是我们平常定义的一个类的实例: /** * Created by aristark on 3/28/16...

    channg 评论0 收藏0
  • 吃透这套Java面试题,拿offer成功率再翻一番

    摘要:语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。有针对不同系统的特定实现,,,目的是使用相同的字节码,它们都会给出相同的结果。 showImg(https://segmentfault.com/img/bVbsjCK?w=800&h=450); 一、面向对象和面向过程的区别 面向过程优点: 性能比面向对象高,因为类调用时需要实...

    elva 评论0 收藏0
  • Java编程中那些再熟悉不过知识点(持续更新)

    摘要:语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。有针对不同系统的特定实现,,,目的是使用相同的字节码,它们都会给出相同的结果。项目主要基于捐赠的源代码。 本文来自于我的慕课网手记:Java编程中那些再熟悉不过的知识点,转载请保留链接 ;) 1. 面向对象和面向过程的区别 面向过程 优点: 性能比面向对象高。因为类调用时需要实例...

    taowen 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<