实验环境
Window 10
java version “1.8.0_191”
Java™ SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot™ 64-Bit
Server VM (build 25.191-b12, mixed mode)
Serviceability Agent 使用简介在此。需要先掌握这个工具的使用。
sa-jdi.jar
在 C:\Program Files\Java\jdk1.8.0_191\lib
目录下。
java -cp sa-jdi.jar sun.jvm.hotspot.HSDB
启动这个工具。
注意:需要把 C:\Program Files\Java\jdk1.8.0_191\bin\sawindbg.dll
复制到 C:\Program Files\Java\jre1.8.0_191\bin
。否则启动报错。
package polymorphic;
class Animal {
public void say() {
System.out.println("Animal say");
}
public void play() {
System.out.println("play...");
}
}
package polymorphic;
class Dog extends Animal {
public static void testStatic(){
System.out.println("testStatic");
}
public void say() {
System.out.println("Dog say");
}
}
package polymorphic;
import java.util.concurrent.CountDownLatch;
public class Demo {
public static void main(String[] args) throws InterruptedException {
Animal a = new Animal(); // 1
a.say();
Dog d = new Dog(); // 2
d.say();
Animal ad = new Dog(); // 3 见字节码部分
ad.say();
CountDownLatch latch = new CountDownLatch(10);// 不让 main 线程退出,便于调试
latch.await();
}
}
Classfile /C:/Users/young/Desktop/hahaha/polymorphic/bin/polymorphic/Demo.class
Last modified 2020-5-30; size 818 bytes
MD5 checksum db790d091c7918640e6b11ad1b467d17
Compiled from "Demo.java"
public class polymorphic.Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // polymorphic/Demo
#2 = Utf8 polymorphic/Demo
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."":()V
#9 = NameAndType #5:#6 // "":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lpolymorphic/Demo;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 Exceptions
#17 = Class #18 // java/lang/InterruptedException
#18 = Utf8 java/lang/InterruptedException
#19 = Class #20 // polymorphic/Animal
#20 = Utf8 polymorphic/Animal
#21 = Methodref #19.#9 // polymorphic/Animal."":()V
#22 = Methodref #19.#23 // polymorphic/Animal.say:()V
#23 = NameAndType #24:#6 // say:()V
#24 = Utf8 say
#25 = Class #26 // polymorphic/Dog
#26 = Utf8 polymorphic/Dog
#27 = Methodref #25.#9 // polymorphic/Dog."":()V
#28 = Methodref #25.#23 // polymorphic/Dog.say:()V
#29 = Class #30 // java/util/concurrent/CountDownLatch
#30 = Utf8 java/util/concurrent/CountDownLatch
#31 = Methodref #29.#32 // java/util/concurrent/CountDownLatch."":(I)V
#32 = NameAndType #5:#33 // "":(I)V
#33 = Utf8 (I)V
#34 = Methodref #29.#35 // java/util/concurrent/CountDownLatch.await:()V
#35 = NameAndType #36:#6 // await:()V
#36 = Utf8 await
#37 = Utf8 args
#38 = Utf8 [Ljava/lang/String;
#39 = Utf8 a
#40 = Utf8 Lpolymorphic/Animal;
#41 = Utf8 d
#42 = Utf8 Lpolymorphic/Dog;
#43 = Utf8 ad
#44 = Utf8 latch
#45 = Utf8 Ljava/util/concurrent/CountDownLatch;
#46 = Utf8 SourceFile
#47 = Utf8 Demo.java
{
public polymorphic.Demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lpolymorphic/Demo;
public static void main(java.lang.String[]) throws java.lang.InterruptedException;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Exceptions:
throws java.lang.InterruptedException
Code:
stack=3, locals=5, args_size=1
0: new #19 // class polymorphic/Animal
3: dup
4: invokespecial #21 // Method polymorphic/Animal."":()V
7: astore_1
8: aload_1
9: invokevirtual #22 // Method polymorphic/Animal.say:()V // 1
12: new #25 // class polymorphic/Dog
15: dup
16: invokespecial #27 // Method polymorphic/Dog."":()V
19: astore_2
20: aload_2
21: invokevirtual #28 // Method polymorphic/Dog.say:()V // 2
24: new #25 // class polymorphic/Dog
27: dup
28: invokespecial #27 // Method polymorphic/Dog."":()V
31: astore_3
32: aload_3
33: invokevirtual #22 // Method polymorphic/Animal.say:()V // 3
36: new #29 // class java/util/concurrent/CountDownLatch
39: dup
40: bipush 10
42: invokespecial #31 // Method java/util/concurrent/CountDownLatch."":(I)V
45: astore 4
47: aload 4
49: invokevirtual #34 // Method java/util/concurrent/CountDownLatch.await:()V
52: return
LineNumberTable:
line 5: 0
line 6: 8
line 7: 12
line 8: 20
line 9: 24
line 10: 32
line 11: 36
line 12: 47
line 13: 52
LocalVariableTable:
Start Length Slot Name Signature
0 53 0 args [Ljava/lang/String;
8 45 1 a Lpolymorphic/Animal;
20 33 2 d Lpolymorphic/Dog;
32 21 3 ad Lpolymorphic/Animal;
47 6 4 latch Ljava/util/concurrent/CountDownLatch;
}
SourceFile: "Demo.java"
只需要看字节码 Code 部分:
9: invokevirtual #22 // Method polymorphic/Animal.say:()V
// 1 对应 a.say(), a 是 Animal 类型33: invokevirtual #22 // Method polymorphic/Animal.say:()V
// 3 对应 ad.say(), ad 是 Dog 类型注意 invokevirtual
调用的方法不是 static
或者 final
类型的。原因很简单,final 类型的方法,根本不允许被继承,自然不会有多态这种东西,因为编译器就能确定调用的是哪一个 final
方法。static
类型的方法也类似。
看到了吗?a
和 ad
虽然是不同类型,但是 a.say()
和 ad.say()
字节码层面调用的都是 Animal.say()
。ad 的引用是 Animal
类型。
在编译时
无法做到 Animal 调用 Animal 的 say 方法,Dog 调用 Dog 的 say 方法。但是实际的运行结果却是Animal 调用 Animal 的 say 方法,Dog 调用 Dog 的 say方法。 这是怎样做到的呢?等程序跑起来,运行起来搞定的。这就是所谓的运行时的多态
。实现的思路非常简单:每一个类都有一个自己的方法表。
一共有 5 个:
终端 jps 查看 java 进程,如下:
C:\Users\young>jps
17664
19572 Demo # demo 进程
4308 HSDB # SA 进程
20232 Jps
我们知道 jvm 启动时,会加载需要的 class。这意味着什么呢?class 文件只是个二进制文件,所谓加载到 jvm 就是将这些二进制数据转换成结构化数据。每个 Java 的 Class 在 JVM 内部都会有一个自己的 instanceKlass,这个 instanceKlass 大小在 64 bit hotspot 下是 0x1b8
。 类的非 static 或者非 final 方法表,就分配在这个 instanceKlass 的后面。
Class Browser 查看 Animal 类在 jvm 中的内存地址
Class Browser 在 Tools 菜单下。
Inspector 查看 Animal Class 的具体信息
Class Browser 得到 Animal 类的 jvm 中的内存地址
。
如上图可以看到 _vtable_len 7
。也就是 Animal 有 7 个非 static 或者非 final 的方法。为啥是 7 个呢?很简单 Object 有 5 个,Animal 中定义了 2 个,所以一共 7 个。
根据 Class 在 jvm 中的内存地址计算方法表的位置:
方法表的位置 = 0x0000000100060218
(Animal Class 的内存地址) + 0x1b8
(instanceKlass 大小) = 0x1000603d0
查看 Dog 类的方法表,和查看 Animal 的过程一样。
Animal:a
mem 0x1000603d0 7
Animal _v_table 方法表
0x00000001000603d0: 0x0000000017790bd8
0x00000001000603d8: 0x00000000177906d0
0x00000001000603e0: 0x0000000017790820
0x00000001000603e8: 0x0000000017790628
0x00000001000603f0: 0x0000000017790760
0x00000001000603f8: 0x0000000017b906c8 # say
0x0000000100060400: 0x0000000017b90770 # play
Dog:ad
mem 0x1000605d0 7
Dog _v_table 方法表
0x00000001000605d0: 0x0000000017790bd8
0x00000001000605d8: 0x00000000177906d0
0x00000001000605e0: 0x0000000017790820
0x00000001000605e8: 0x0000000017790628
0x00000001000605f0: 0x0000000017790760
0x00000001000605f8: 0x0000000017b90b08 # say 重写了,这个地址是 Dog 类重写的 say 方法的地址
0x0000000100060600: 0x0000000017b90770 # play 没有重写,这个地址是 Animal 的 play 方法的地址
可以看到 Animal 类中 say 方法的地址和 Dog 类中 say 方法的地址根本不一样
。这就是重点。每一个类都握有自己的一个方法表。而这些方法的地址是只有在程序运行起来才可以得到
。这其实就是运行时多态的实现方式,或者叫做运行时绑定。
每一个类都有自己的方法表。子类重写父类方法,虽然子类和父类的方法名相同。但是由于每一个类都有自己的方法表,所以不同类的对象,在运行时都可以找到自己的类的方法。这就是所谓面向接口编程的基础。定义了接口,但是有不同的实现类,每一个实现类都有自己实现类的方法表。互不相干。java 程序在运行起来之后,不会找不到自己类实现的方法。
package poly;
public interface Animal {
void say();
}
package poly;
public class Cat implements Animal {
@Override
public void say() {
System.out.println("I'm a cat");
}
}
package poly;
public class Dog implements Animal {
@Override
public void say() {
System.out.println("I'm a dog");
}
}
package poly;
public class Demo {
public static void main(String[] args) {
Animal cat = new Cat(); // 拿着 Animal 类型的引用就能访问所有实现 Animal 接口的实现类的 say 方法。
cat.say();
cat = new Dog();
cat.say();
}
}