用反射覆盖最终静态字段有限制吗



在我的一些单元测试中,我遇到了在最终静态字段上反射的奇怪行为。下面的例子说明了我的问题。

我有一个基本的Singleton类,它包含一个Integer

public class BasicHolder {
private static BasicHolder instance = new BasicHolder();
public static BasicHolder getInstance() {
return instance;
}
private BasicHolder() {
}
private final static Integer VALUE = new Integer(0);
public Integer getVALUE() {
return VALUE;
}
}

我的测试用例是循环并通过Reflection将VALUE设置为迭代索引,然后断言VALUE完全等于迭代索引。

class TestStaticLimits {
private static final Integer NB_ITERATION = 10_000;
@Test
void testStaticLimit() {
for (Integer i = 0; i < NB_ITERATION; i++) {
setStaticFieldValue(BasicHolder.class, "VALUE", i);
Assertions.assertEquals(i, BasicHolder.getInstance().getVALUE(), "REFLECTION DID NOT WORK for iteration "+i);
System.out.println("iter " + i + " ok" );
}
}
private static void setStaticFieldValue(final Class obj, final String fieldName, final Object fieldValue) {
try {
final Field field = obj.getDeclaredField(fieldName);
field.setAccessible(true);
final Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, fieldValue);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("Error while setting field [" + fieldName + "] on object " + obj + " Message " + e.getMessage(), e);
}
}
}

结果非常令人惊讶,因为它不是恒定的,我的测试在大约1000次迭代中失败了,但它似乎从来都不一样。

有人已经面临过这个问题吗?

JLS提到在构造后修改最终字段是有问题的-请参阅17.5.最终字段语义

声明为final的字段初始化一次,但在正常情况下从未更改。final字段的详细语义与普通字段的语义有些不同。特别是,编译器有很大的自由度来跨越同步屏障和对任意或未知方法的调用来移动对最终字段的读取。相应地,编译器可以将最终字段的值缓存在寄存器中,而不是在必须重新加载非最终字段的情况下从内存中重新加载。

和17.5.3。最终字段的后续修改:

另一个问题是规范允许对最终字段进行积极的优化。在线程中,允许使用未在构造函数中发生的对最终字段的修改来重新排序最终字段的读取。

除此之外,Field.set的JavaDocs还包括一个关于此的警告:

只有在反序列化或重建具有空白最终字段的类实例的过程中,在程序的其他部分可以访问这些实例之前,以这种方式设置最终字段才有意义。在任何其他上下文中使用都可能产生不可预测的影响,包括程序的其他部分继续使用此字段的原始值的情况。

我们在这里看到的似乎是JIT利用了语言规范赋予的重新排序和缓存的可能性。

这是因为JIT优化。要证明这一点,请使用以下VM选项禁用它:

-Djava.compiler=NONE

在这种情况下,所有10_000迭代都将起作用。

或者,将BasicHolder.getVALUE方法排除在编译之外:

-XX:CompileCommand=exclude,src/main/BasicHolder.getVALUE

实际情况是,在nth迭代之后,正在编译热方法getVALUE,并且正在积极优化static final Integer VALUE(这实际上是即时常数1(。从这一点开始,断言开始失败。

-XX:+PrintCompilation的输出和我的评论:

val 1       # System.out.println("val " + BasicHolder.getInstance().getVALUE());
val 2
val 3
...
922  315    3    src.main.BasicHolder::getInstance (4 bytes)   # Method compiled
922  316    3    src.main.BasicHolder::getVALUE    (4 bytes)   # Method compiled
...
val 1563    # after compilation
val 1563
val 1563
val 1563
...

1-JVM Anatomy Park:实时常数

最新更新