Why bytecode calls Object->getClass() at a direct

2019-01-28 14:11发布

I decompiled Java (actually Dalvik) bytecode. In the beginning of a method, I access a field of an instance member directly (i.e. not through a getter).

It seems tha Java calls Object.getClass() on the accessed instance member (mOther), but doesn't use the result anywhere. Is this some sort of check? Why is this call needed? I suspect it is because I access a field directly (which is defined in that class), but I don't see the connection.

The Java code and the decompiled bytecode are as follows.

(Note that the last instruction loads lifeTime as constant 0x0001 because in MyOtherClass, I have lifeTime as a public final field, and is currently initialized from code.)

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

UPDATE:

It was requested in comments that I provide the method's full source code. Note that mOther is a final field (for performance reasons). Here you're:

@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 < 0 || bx >= mSize) { continue; }
            for (int by = by0; by <= by1; ++by) {
                if (by < 0 || by >= mSize) { continue; }
                ArrayList<WorldObject> 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; }
    }
}

2条回答
做个烂人
2楼-- · 2019-01-28 14:21

I'm assuming lifeTime is a final field that is assigned upon declaration:

 final int lifeTime = 0x0001;

If so, the bytecode is optimized in the following way (it has next to nothing to do with the VM, pure compiler magic):

  • There's no need to really fetch data from memory: all that's needed is to load a constant 1.
  • But what if the owner of the field happens to be null? In this case a NullPointerException must be thrown. To guarantee such behavior compilers emit calls to getClass(), because
    • actually checking for null, constructing a new instance of NullPointerException and throwing it is a lot more byte code,
    • such calls are very optimized in the VM,
    • this method is always available,
    • it takes no arguments.

A simpler example:

class Test {
    private final int myFinalField = 1;

    int test(Test t) {
        return t.myFinalField;
    }
}

If we look at the byte codes of the test() method (JVM this time, but should you translate it to Dalvik, it will be essentially the same), here is a call to getClass() too:

 // 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
查看更多
迷人小祖宗
3楼-- · 2019-01-28 14:31

Andrey's answer gives the specific answer for this question. But here are a couple of notes relevant to this kind of question:

  1. Apparently, you can see a similar thing with bytecodes produced by the Oracle / OpenJDK tool chain. This is not that surprising, since some generation paths for Davlik bytecodes involve compiling Java source code to JVM bytecodes and then translating them to Davlik bytecodes.

  2. If you have come across this odd artifact because you were looking at the bytecodes to get insight into the performance some code, then you are probably looking in the wrong place. In a modern JVM / Davlik / ART engine, the bytecodes are translated into native code, and the native code is what gets executed most or all of the time1.

    To get a more reliable insight into code performance at the "micro" level, you need to examine the native code produced by the AOT or JIT compiler.

  3. One of the reasons that bytecodes emitted by a bytecode compiler are typically not heavily optimized is that doing that could make it more difficult for the AOT / JIT to optimize effectively.

  4. Davlik has been superseded by ART.


1 - With Hotspot JVMs, only JIT and direct bytecode interpretation are supported. Early versions of Davlik were interpret-only, and then JIT support was added, and improved. In ART, all three modes are supported in some form: interpret, JIT and AOT.

查看更多
登录 后发表回答