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

sorra 发表于 09/28 13:04 修改于 09/28 13:16 阅读数73

某个测试服务器试图通过反射来修改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/specs/jls/se7/html/jls-15.html#jls-15.28

然后我就发现一个危险现象:引用自另一个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/2014/01/javac-optimizations.html