【问题标题】:Why does the Java compiler inline access to non-static final fields?为什么 Java 编译器内联访问非静态 final 字段?
【发布时间】:2017-10-28 23:55:57
【问题描述】:

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

给定一个定义

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

直接访问field (state.field) 生成

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

但直接访问finalField (state.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 字段的值的基本原理是什么?

【问题讨论】:

  • 很清楚为什么简单地替换为恒定负载不起作用,并且需要一些东西来验证对象引用不为空。但它仍然是一个有趣的问题,为什么它会被内联 - 为什么用方法调用加上恒定负载来替换字段访问?为什么编译器不只是将字段访问留在那里?这种方式不算什么优化。
  • 缺点是 JavaState 类中的更改不会传播到依赖类。由于 HotSpot 是一项古老的技术,因此任何此类优化都可以在 JVM 级别完成,而不会破坏代码的模块化。是时候从源编译器中删除这种“优化”了吗?
  • 我已经用 getClass 信息更新了正文 - 我认为问题 Why does the Java compiler inline access to non-static final fields? 仍然存在。
  • 我会注意到您没有显示代码“直接访问field”。它住在哪里?
  • 我的猜测是,在 JIT 上,getClass 上的 invokeVirtual 将在重复时被丢弃。 Raspberry Pi 可能会出现一些效率低下的问题。

标签: java jvm javac


【解决方案1】:

这可能是 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 节)。

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

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

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

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

由于非静态常量 final 字段不常用也不是特别有用,因此在编写规范时它们可能只是从裂缝中溜走了。

【讨论】:

  • 一个很好的答案谢谢。代码来自课外,所以,长话短说,“因为 JLS 是这么说的”。
  • 对第三段的解释是,对该字段的唯一代码引用应该在初始化该字段的<init>方法中,这是正确的,并且已经在第一句中暗示:A reference to a作为常量变量的字段(第 4.12.4 节)必须在编译时解析为由常量变量的初始化程序表示的值 V
  • 顺便说一句,这个规则不仅与问题无关,变量是static还是实例字段,它是字段还是局部变量也无关紧要。你甚至可以使用局部变量作为注解值or case labels,如果它们是常量变量的话。
  • 此指定行为的一个重要方面是所有常量都不受初始化顺序问题的影响,例如如果超类构造函数在子类的字段初始值设定项尚未执行时调用重写的方法。或者当涉及没有适当同步的多线程时。另见this answer...
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多