为什么Java编译器内联访问非静态最终字段?

时间:2017-10-28 23:55:58

标签: java jvm javac

我一直在运行一些微基准测试,并且遇到了一个奇怪的问题。我使用java version "1.8.0_131"和默认的编译器选项。

给定定义

public class JavaState {
    public String field = "hello";
    public final String finalField = "hello";
}

直接访问fieldstate.field)生成

ALOAD 1
GETFIELD JavaState.field : Ljava/lang/String;

但直接访问finalFieldstate.finalField)会生成

ALOAD 1
INVOKEVIRTUAL java/lang/Object.getClass ()Ljava/lang/Class;
POP
LDC "hello"

Why bytecode calls Object->getClass() at a direct field access解释说,调用getClass只是为了检查state是不是null,但编译器已经内联了该字段的值。

我可能合理地期望用不同的字段值替换更高版本的JavaState会导致其他代码在没有重新编译的情况下看到更改,但是这种内联可以防止这种情况发生。而我的基准测试表明,如果以性能的名义完成,它就不起作用;至少在我的基准测试Raspberry Pi上,访问finalField比访问field慢5-10%。

内联final字段的价值的理由是什么?

1 个答案:

答案 0 :(得分:7)

这可能是Java语言规范的要求,但细节尚不清楚。来自Section 4.12.4 final Variables

  

常量变量是基本类型或类型String的最终变量,使用常量表达式(第15.28节)进行初始化。变量是否是常量变量可能对类初始化(第12.4.1节),二进制兼容性(第13.1节,第13.4.9节)和明确赋值(第16节(定义赋值))有影响。

请注意,不要求变量为static。然后从Section 13.1 The Form of a Binary

  

对于作为常量变量(§4.12.4)的字段的引用必须在编译时解析为由常量变量的初始化程序表示的值V.

     

如果这样的字段是静态的,[...]

     

如果这样的字段是非静态的,那么除了包含该字段的类之外,二进制文件中的代码中不应该存在对该字段的引用。 (它将是一个类而不是一个接口,因为一个接口只有静态字段。)该类应该有代码在实例创建期间将字段的值设置为V(§12.5)。

我不确定您的反编译代码来自哪里。如果它在课外,那么您所看到的是规范要求的。如果它在课堂内,那就不太清楚了。您可以阅读上面引用中的第三段,以暗示该字段的代码引用应该在初始化字段的<init>方法中,但实际上并未说明。

Section 13.4.9直接解决了您对二进制兼容性的担忧,但似乎明确地将自己限制为static字段(强调我的):

  

如果字段是常量变量(§4.12.4),而且是静态,则删除关键字final或更改其值不会破坏与预先存在的二进制文件的兼容性不运行,但除非重新编译,否则他们不会看到使用该字段的任何新值。该结果是决定支持条件编译的副作用(§14.21)。 (有人可能会认为,如果用法出现在常量表达式中(第15.28节),则看不到新值。但事实并非如此。事实上,预先存在的二进制文件根本看不到新值。)

     

要求内联静态常量变量值的另一个原因是switch语句。它们是唯一依赖于常量表达式的语句,即switch语句的每个case标签必须是一个常量表达式,其值与每个其他case标签不同。大小写标签通常是对静态常量变量的引用,因此可能不会立即显示所有标签具有不同的值。如果证明在编译时没有重复的标签,那么将值内联到类文件中可确保在运行时没有重复的标签 - 非常理想的属性。

由于非静态常数最终字段不常用,也不是特别有用,因此在编写规范时,它们可能只是滑过裂缝。