【JVM】--实例分析override实现原理
0x00 多态|方法重写实例
首先上例子,有Java语言基础的都能看懂,并且能够知道输出结果是什么。
package com.zhb.test;
public class OverrideTest {
public static void main(String[] args) {
Father son1 = new Son1();
Father son2 = new Son2();
son1.saySth("something");
son2.saySth("something");
}
}
class Father {
public void saySth(String sth){
System.out.println("father say :" + sth);
}
}
class Son1 extends Father {
public void saySth(String sth) {
System.out.println("son1 say :" + sth);
}
}
class Son2 extends Father {
public void saySth(String sth) {
System.out.println("son2 say :" + sth);
}
}
输出结果如下:

这不难理解,这就是典型的Java的多态。多态的三要素:继承、重写、向上转型(父类引用指向子类对象)。
0x01 从字节码的角度分析
对0x00节的代码在IDEA终端(CMD或者Linux终端都可)中运行如下两条命令:
javac OverrideTest.java
javap -v -p OverrideTest 在IDEA中得到如下输出(反编译后的字节码):
警告: 二进制文件OverrideTest包含com.zhb.test.OverrideTest
Classfile /Users/zhb/IdeaProjects/test/src/main/java/com/zhb/test/OverrideTest.class
Last modified 2021-1-19; size 465 bytes
MD5 checksum 32fc082cff5dbe9da6bd5984a37de997
Compiled from "OverrideTest.java"
public class com.zhb.test.OverrideTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#18 // java/lang/Object."<init>":()V
#2 = Class #19 // com/zhb/test/Son1
#3 = Methodref #2.#18 // com/zhb/test/Son1."<init>":()V
#4 = Class #20 // com/zhb/test/Son2
#5 = Methodref #4.#18 // com/zhb/test/Son2."<init>":()V
#6 = String #21 // something
#7 = Methodref #22.#23 // com/zhb/test/Father.saySth:(Ljava/lang/String;)V
#8 = Class #24 // com/zhb/test/OverrideTest
#9 = Class #25 // java/lang/Object
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 SourceFile
#17 = Utf8 OverrideTest.java
#18 = NameAndType #10:#11 // "<init>":()V
#19 = Utf8 com/zhb/test/Son1
#20 = Utf8 com/zhb/test/Son2
#21 = Utf8 something
#22 = Class #26 // com/zhb/test/Father
#23 = NameAndType #27:#28 // saySth:(Ljava/lang/String;)V
#24 = Utf8 com/zhb/test/OverrideTest
#25 = Utf8 java/lang/Object
#26 = Utf8 com/zhb/test/Father
#27 = Utf8 saySth
#28 = Utf8 (Ljava/lang/String;)V
{
public com.zhb.test.OverrideTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class com/zhb/test/Son1
3: dup
4: invokespecial #3 // Method com/zhb/test/Son1."<init>":()V
7: astore_1
8: new #4 // class com/zhb/test/Son2
11: dup
12: invokespecial #5 // Method com/zhb/test/Son2."<init>":()V
15: astore_2
16: aload_1
17: ldc #6 // String something
19: invokevirtual #7 // Method com/zhb/test/Father.saySth:(Ljava/lang/String;)V
22: aload_2
23: ldc #6 // String something
25: invokevirtual #7 // Method com/zhb/test/Father.saySth:(Ljava/lang/String;)V
28: return
LineNumberTable:
line 7: 0
line 8: 8
line 9: 16
line 10: 22
line 11: 28
}
SourceFile: "OverrideTest.java"
我们关注main方法的反编译过后的字节码(看不懂字节码不要紧在图中大致已经标注出对应字节码的功能了,看不懂,其实也不影响继续阅读)

首先是new了Son1对象赋给son1引用,然后new了Son2对象赋给son2引用。接着把son1引用加载到操作数栈上,然后ldc这条字节码指令把常量池#6的String给push到操作数栈上,接着调用invokevirtual指令。后面的就是对son2进行同样的过程,因此略过,最后调用return指令返回。
从上面反编译的字节码中可以看到,invokevirtual后面都是#7,如下图所示:

也就是invoke指令后,都是Father.saySth的方法引用。但是为什么最后输出结果不同呢?这就涉及到invokevirtual指令的语义了。关于invokevirtual指令,准备后续专门写一篇文章专门详细分析,在这里只需要知道为什么能够准确找到相应子类的重写方法就好了。
通过查询JVM规范中的invokevirtual指令。
invokevirtual指令,Java虚拟机规范网址
知道invokevirtual会针对#7这个方法引用进行如下操作:
note:#7是resolved method
-
找到操作数栈顶的第一个元素所指向的对象的实际类型(本例中第一个invokevirtual指令所对应的是com/zhb/test/Son1),记作C。
-
如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。
-
如果在C中没找到,则按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。
-
如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
正是因为invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的 invokevirtual指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,还会根据方法接收者 的实际类型来选择方法版本,这个过程就是Java语言中方法重写的本质。
参考资料
- https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.invokespecial
- 《深入理解Java虚拟机 第三版》--周志明