invokevitual指令的疑惑?

public class test2 {
    static class Father {
        public int money = 1;

        public Father() {
            money = 2;
            showMeTheMoney();
        }

        public void showMeTheMoney() {
            System.out.println("I am Father, i have $" + money);
        }
    }

    static class Son extends Father {
        public int money = 3;

        public Son() {
            money = 4;
            showMeTheMoney();
        }

        public void showMeTheMoney() {
            System.out.println("I am Son, i have $" + money);
        }
    }

    public static void main(String[] args) {
        Father guy = new Son();
    }
}

这段代码输出为
image.png
为什么会是这个输出,我希望大佬们能通过字节码指令的方式教教我。

image.png
这是我得到的main方法的字节码指令
《深入理解JAVA虚拟机 第三版》周志明著中写道invokevirtual是调用对象实例的方法,我很想知道在为什么他会调用Son的showMeTheMoney,他传的对象实例都是Son吗?
新的问题补充:
main方法被调用时创建了自己的栈帧,而Son的init方法和Father的init方法被调用时也都有自己栈帧,invokevirtual拿到的栈帧中操作数栈最顶端的对象实例,但是我查看它们的字节码反汇编版本,看到他们的局部变量槽最大数为1,那么也就是说只有this能上到操作数栈中,那么Son的实例是怎么在Father的init方法中被showMeTheMoney方法调用的。

阅读 1.2k
3 个回答

要通过字节码指令的方式修改代码,可以使用 javassist 或 ASM 这样的字节码操作库。以下是使用 javassist 修改代码的示例:

import javassist.*;

public class ModifyClass {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.get("test2$Son");

        // 修改构造函数
        CtConstructor constructor = cc.getDeclaredConstructor(null);
        constructor.insertBefore("{ money = 4; showMeTheMoney(); }");

        // 修改 showMeTheMoney 方法
        CtMethod method = cc.getDeclaredMethod("showMeTheMoney");
        method.setBody("{ System.out.println(\"I am Son, i have $\" + money); }");

        cc.writeFile(); // 将修改后的类写入文件
    }
}

补充

简化的字节码分析:

// Father constructor
0: aload_0         // load 'this' onto the stack
1: invokespecial #1 // call super() (Object's constructor)
4: aload_0         // load 'this' onto the stack
5: iconst_2        // push constant 2 onto the stack
6: putfield #2     // set 'money' field of 'this' to 2
9: aload_0         // load 'this' onto the stack
10: invokevirtual #3 // call showMeTheMoney() on 'this'
13: return

// Son constructor
0: aload_0         // load 'this' onto the stack
1: invokespecial #1 // call super() (Father's constructor)
4: aload_0         // load 'this' onto the stack
5: iconst_4        // push constant 4 onto the stack
6: putfield #2     // set 'money' field of 'this' to 4
9: aload_0         // load 'this' onto the stack
10: invokevirtual #3 // call showMeTheMoney() on 'this'
13: return

在 Father 的构造函数中,aload_0 将 this 引用加载到操作数栈顶,invokevirtual 使用这个引用来调用 showMeTheMoney() 方法。由于 this 实际上是 Son 的实例,因此调用的是 Son 的 showMeTheMoney() 方法。

由于Java是动态绑定的,所以是根据对象的实际类型(Son)来决定调用哪个方法。所以即使是在Father的构造函数中调用showMeTheMoney(),最终执行的是Son中的实现。
而且在Father的构造函数中,Son对象的money属性还没有初始化所以输出i have $0

你的字节码应该是对的


Father guy = new Son()。触发了Son的初始化,由于其父类Father还没初始化,所以Father先初始化,Father初始化时(Son的加载、验证、准备已经完成了,静态解析完成,Father也相应的完成了),然后Father中的<init>方法中调用invokevirtual指令,这个指令是怎么与实际类型Son联系起来的,也就是它是怎么获得这个Son实际类型的。

当执行Father guy = new Son();时,new Son()创建Son实例,Son构造函数被调用;在调用之前,jvm会先调用Father构造函数。在Father构造函数中调用了showMeTheMoney(),使用了invokevirtual指令。这个指令会根据当前对象的实际类型来决定调用的方法。虽然这是在Father构造函数中,但由于创建的是一个Son对象,所以jvm会调用Son的重写方法。

查看字节码时,会看到类似这样的指令表示调用父类构造函数

invokespecial #3 <test2$Father.<init> : ()V>

当执行到showMeTheMoney()时会有类似于这样的指令,此时jvm会检查实际类型(Son)并找到Son.showMeTheMoney()

invokevirtual #4 <test2$Father.showMeTheMoney : ()V>

在每个方法调用时方法都会有自己的栈帧。每个栈帧中this引用会被存储在局部变量槽中。在父类构造函数中使用showMeTheMoney()时,this指向的是Son的实例。所以在Father的构造函数中,使用invokevirtual showMeTheMoney()仍然能够正确解析到Son.showMeTheMoney()
java是基于实例化后的对象来进行方法解析的,这好像叫动态绑定机制

首先感谢水蒸气、Seven两位大佬的帮助,我又翻看了《Java虚拟机规范》中关于invokespecial与incokevirtual指令的介绍。我现在大概捋清了思路。该思路如下:Father guy = new Son();这里有三条指令:

0 new #2 <test2$Son> //在java堆中创建了一个Son对象实例,并将它的引用压入到操作数栈顶
3 dup               //复制操作数栈顶的对象,将其压入到操作数栈顶,这时就有两个son引用
4 invokespecial #3 <test2$Son.<init> : ()V> //取操作数栈顶的引用,调用<init>

invokespecial指令调用后会创建一个栈帧,该栈帧局部变量表中的第0号变量槽会存储Son的对象实例引用,也就是"this"。接下来我们看Son与Father<init>方法的字节码反汇编结果:

 0 aload_0  //将局部变量表中的'this'加载到操作数栈顶
 1 invokespecial #1 <test2$Father.<init> : ()V> //和之前一样的virtualspecial指令
 4 aload_0
 5 iconst_3
 6 putfield #2 <test2$Son.money : I>
 9 aload_0
10 iconst_4
11 putfield #2 <test2$Son.money : I>
14 aload_0
15 invokevirtual #3 <test2$Son.showMeTheMoney : ()V>
18 return
// Father类的<init>方法大差不差,就改了些iconst_n中n的数字

这次invokespecial指令调用了'this'也就是Son的对象实例,那么在Father<init>的栈帧中的局部变量表的第0号变量槽就是'this'。这时Father<init>中的invokevirtual指令调用的就是操作数栈顶的'this'引用变量,即对son.showMeTheMoney的使用。invokevirtual指令也有许多判定流程,这里可以满足Son.showMeTheMoney是对Father.showMeTheMoney的方法覆盖。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏