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

在这里类加载的过程就不多说了,主要讲一下类加载中的解析过程。《Java虚拟机规范》中规定其必须在执行invokespecial、invokevirtual等17个字节码指令之前完成,那么其执行的时间就不确定了。这次我们再认识一点invokespecial与invokevirtual的知识(因为作者也只是学到了一点,通过对《深入理解Java虚拟机》一些作者也搞不懂的例子进行展开)。
编译期的解析就只有5个:静态方法、<init>方法、私有方法和父类中的方法还有用final修饰的方法。
分派分为静态分派与动态分派。静态分派:根据对象的静态类型决定执行方法的版本,那么动态分派就应该是根据实际类型。分派是在运行期的解析。
以下指令介绍我只选取了对于该实例有用的部分。
invokespecial指令:
作用:调用实例方法,专门用来调用父类方法、 私有方法和实例初始化方法
要注意invokespecial指令取出操作数栈顶的对象实例引用与几个方法需要的参数后,在后面调用方法栈帧中的局部变量槽中还要依次存入。
invokevirtual指令:
作用:用实例方法,依据实例的类型进行分派
假设C是objectref所对应的类,虚拟机将按下面规则查找实际要执行的方法:
(1)如果C中定义了一个实例方法m,该方法覆盖了符号引用中
表示的方法,那么方法m就会被调用,查找过程终止
(2)否则,如果C有父类,查找过程将按第1点的方式顺序递归搜索c的直接父
类和父类的父类,直至搜索到能够覆盖解析出来的那个方法的某条实例方法
声明,或再也找不到父类时为止。 如果找到了某个覆盖方法,那么这个方法
就会被调用
(3)否则,如果C的超接口中有且只有一个与解析出来的方法具备相同名称及描
述符,而又不是abstract 方法的最具体方法,那么此方法就是待调用的方法
方法覆盖的条件:
对于声明在类C中的实例方法me,以及声明在类A中的另一个实例方法ma来说,当且
仅当me与ma相同,或下列条件均满足时, me才能覆盖ma:
(1)c是A的子类。
(2)me与ma拥有相同的名称及方法描述符。
(3)me没有标注为ACC PRIVATE。
(4)下面其中一条成立:
ma的权限符是ACC_PUBLIC、 ACC_PROTECTED 或默认权限(也就是既不是
ACC_PUBLIC,也不是ACC_PROTECTED,更不是ACC_PRIVATE 的那种权限),
且A类和C类处于同一运行时包下面。
me覆盖方法m',(m'与me不同,也与ma不同),并且m'覆盖了mq。
上述是前置知识。接下来我们利用该类的字节码来解析invokespecial与invokevirtual的部分面貌。
这是Test2.main方法中的字节码

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

这是Son的部分构造器的字节码
对重要部分进行了标注

 0 aload_0 //将该栈帧中局部变量表第0号变量槽中的引用压入到操作数栈顶
 1 invokespecial #1 //取出操作数栈顶的引用,用该引用调用父类的<init>方法
 4 aload_0
 5 iconst_3
 6 putfield #2
 9 aload_0
10 iconst_4
11 putfield #2
14 aload_0
15 invokevirtual #3
18 return

这是Father的部分构造器的字节码

 0 aload_0 
 1 invokespecial #1 //这是对Object<init>方法的调用
 4 aload_0
 5 iconst_1
 6 putfield #2
 9 aload_0
10 iconst_2
11 putfield #2 
14 aload_0
15 invokevirtual #3 //调用showMeTheMoney()
18 return

看到上面Father的字节码是不是很熟悉,现在aload_0这个指令非常重要,因为栈帧中局部变量表中的第0号变量槽存储的是Son对象实例。然后我们一路看到15 invokevirtual #3,此时由于前面的操作,操作数栈顶的是Son对象实例,我们调用了Son对象实例的调用showMeTheMoney()(Son.showMeTheMoney()是对Father.showMeTheMoney()的方法覆盖,条件都满足,invokevirtual指令在第一步就直接结束了查找,都不用向父类查找了)。至此,我们的分析其实已经很明了了。
作者第一次文章,学习JVM的时间2周,所以经验不多,如果文章中有错误,希望各位读者提出建议。同时有更进一步或者更加清晰的说法请一定要说出来,我也很想进步!!!

《深入理解Java虚拟机》 周志明著
《Java虚拟机规范》
各位帮助我解决问题的好人们

痛苦的手链_dykgOq
9 声望2 粉丝