为什么字节码在直接字段访问时调用Object-> getClass()

我反编译Java(实际上是Dalvik)字节码。 在方法的开头,我直接访问实例成员的字段(即不通过getter)。

看来Java在被访问的实例成员( Object.getClass()上调用了Object.getClass() ),但是没有在任何地方使用结果。 这是某种检查吗? 为什么需要这个电话? 我怀疑这是因为我直接访问一个字段(在该类中定义),但我没有看到连接。

Java代码和反编译的字节码如下。

(注意,最后一条指令将lifeTime加载为常量0x0001因为在MyOtherClass ,我将lifeTime作为public final字段,并且当前从代码初始化。)

 MyOtherClass other = mOther; if (mAge >= other.lifeTime) { // lifeTime is initialized to 0x0001 end(); return; } .line 53 move-object/from16 v0, p0 iget-object v0, v0, Lcom/example/engine/MyClass1;->mOther:Lcom/example/engine/MyOtherClass; move-object/from16 v16, v0 .line 54 .local v16, other:Lcom/example/engine/MyOtherClass; move-object/from16 v0, p0 iget v0, v0, Lcom/example/engine/MyClass1;->mAge:I move/from16 v18, v0 // Why is Object->getClass() called? invoke-virtual/range {v16 .. v16}, Ljava/lang/Object;->getClass()Ljava/lang/Class; const/16 v19, 0x0001 

更新:

在评论中要求我提供该方法的完整源代码。 请注意, mOther是最终字段(出于性能原因)。 你在这里:

 @Override public void doStep() { MyOtherClass other = mOther; if (mAge >= other.lifeTime) { end(); return; } mAge += TICK_TIME; boolean isSurrounded = false; if (mAge > mLastSurroundTime + other.surroundingTime) { int distance = (int)other.maxSurroundDistance; for (int bx = bx0; bx <= bx1; ++bx) { if (bx = mSize) { continue; } for (int by = by0; by <= by1; ++by) { if (by = mSize) { continue; } ArrayList candidates = getCandidatesAtPos(bx, by); for (int i = 0; i < candidates.size(); ++i) { WorldObject obj = candidates.get(i); if (mSelf!= obj && mSelf.getDistanceFrom(obj) <= other.maxSurroundDistance) { obj.notifyDangerImminent(mSelf); isSurrounded = true; } } } } if (isSurrounded) { mLastSurroundTime = mAge; } } } 

我假设lifeTime是一个在声明时分配的最终字段:

  final int lifeTime = 0x0001; 

如果是这样,字节码将按以下方式进行优化(它与VM无关,纯编译器魔术):

  • 没有必要真正从内存中获取数据:所需的只是加载常量1。
  • 但是,如果该字段的所有者恰好是空的呢? 在这种情况下,必须抛出NullPointerException。 为了保证这种行为,编译器会发出对getClass()的调用,因为
    • 实际上检查null,构造一个新的NullPointerException实例并抛出它是更多的字节码,
    • 这些调用在VM中非常优化,
    • 这种方法总是可用的,
    • 它不需要任何参数。

一个更简单的例子:

 class Test { private final int myFinalField = 1; int test(Test t) { return t.myFinalField; } } 

如果我们查看test()方法的字节代码(这次是JVM,但是你应该把它翻译成Dalvik,它实际上是相同的),这里也是对getClass()的调用:

  // access flags 0x0 test(LTest;)I L0 LINENUMBER 5 L0 // load t ALOAD 1 // if (t == null) throw new NullPointerException(); compressed in only two instructions INVOKEVIRTUAL java/lang/Object.getClass ()Ljava/lang/Class; POP // the actual value of myFinalField ICONST_1 IRETURN L1 LOCALVARIABLE this LTest; L0 L1 0 LOCALVARIABLE t LTest; L0 L1 1 MAXSTACK = 1 MAXLOCALS = 2 

安德烈的回答给出了这个问题的具体答案。 但这里有几个与此类问题相关的注释:

  1. 显然,您可以看到Oracle / OpenJDK工具链生成的字节码类似的东西。 这并不奇怪,因为Davlik字节码的一些生成路径涉及将Java源代码编译为JVM字节码,然后将它们转换为Davlik字节码。

  2. 如果您遇到这个奇怪的工件,因为您正在查看字节码以深入了解某些代码的性能,那么您可能正在寻找错误的位置。 在现代JVM / Davlik / ART引擎中,字节码被转换为本机代码,本机代码是大多数或所有时间执行的代码1

    为了更加可靠地了解“微”级别的代码性能,您需要检查AOT或JIT编译器生成的本机代码。

  3. 字节码编译器发出的字节码通常没有经过大量优化的原因之一是,这样做可能会使AOT / JIT更难以有效地进行优化。

  4. Davlik已被ART取代。


1 – 使用Hotspot JVM,仅支持JIT和直接字节码解释。 早期版本的Davlik只是解释性的,然后JIT支持被添加和改进。 在ART中,所有三种模式都以某种forms支持:解释,JIT和AOT。