Search code examples
javaandroidbytecodedalvik

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


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; }
    }
}

Solution

  • 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