我们写代码时方法调用是最常见的场景,但是这种最常见的场景在JVM中是如何实现的呢,下面就一起来探索一番。
注:博客内容参考了周志明的《深入理解java虚拟机》,如果大家想了解的更详细推荐这本书,另外还有一本比较久远的《深入java虚拟机第二版》也一并推荐给大家,这本书是外国人写的一本,关于jvm的结构,class文件结构,字节码,垃圾回收等等做了非常详细的讲解。
class文件的编译过程不包含传统编译中的连接步骤,方法在class文件中只是存储的符号引用,而不是方法在内存中的入口地址,这个特性为java提供了强大的动态扩展能力,先来看一下一个javap反编译后的文件。
Compiled from "Singleton.java"
public class Singleton extends java.lang.Object
SourceFile: "Singleton.java"
minor version: 0
major version: 50
Constant pool:
const #1 = Method #10.#27; // java/lang/Object."<init>":()V
const #2 = Field #8.#28; // Singleton.a:I
const #3 = Field #8.#29; // Singleton.b:I
const #4 = Field #8.#30; // Singleton.instance:LSingleton;
const #5 = Method #8.#31; // Singleton.getInstance:()LSingleton;
const #6 = Field #32.#33; // java/lang/System.out:Ljava/io/PrintStream;
const #7 = Method #34.#35; // java/io/PrintStream.println:(I)V
const #8 = class #36; // Singleton
const #9 = Method #8.#27; // Singleton."<init>":()V
const #10 = class #37; // java/lang/Object
const #11 = Asciz instance;
const #12 = Asciz LSingleton;;
const #13 = Asciz a;
const #14 = Asciz I;
const #15 = Asciz b;
const #16 = Asciz <init>;
const #17 = Asciz ()V;
const #18 = Asciz Code;
const #19 = Asciz LineNumberTable;
const #20 = Asciz getInstance;
const #21 = Asciz ()LSingleton;;
const #22 = Asciz main;
const #23 = Asciz ([Ljava/lang/String;)V;
const #24 = Asciz <clinit>;
const #25 = Asciz SourceFile;
const #26 = Asciz Singleton.java;
const #27 = NameAndType #16:#17;// "<init>":()V
const #28 = NameAndType #13:#14;// a:I
const #29 = NameAndType #15:#14;// b:I
const #30 = NameAndType #11:#12;// instance:LSingleton;
const #31 = NameAndType #20:#21;// getInstance:()LSingleton;
const #32 = class #38; // java/lang/System
const #33 = NameAndType #39:#40;// out:Ljava/io/PrintStream;
const #34 = class #41; // java/io/PrintStream
const #35 = NameAndType #42:#43;// println:(I)V
const #36 = Asciz Singleton;
const #37 = Asciz java/lang/Object;
const #38 = Asciz java/lang/System;
const #39 = Asciz out;
const #40 = Asciz Ljava/io/PrintStream;;
const #41 = Asciz java/io/PrintStream;
const #42 = Asciz println;
const #43 = Asciz (I)V;
{
public static Singleton instance;
public static int a;
public static int b;
private Singleton();
Code:
Stack=2, Locals=1, Args_size=1
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: getstatic #2; //Field a:I
7: iconst_1
8: iadd
9: putstatic #2; //Field a:I
12: getstatic #3; //Field b:I
15: iconst_1
16: iadd
17: putstatic #3; //Field b:I
20: return
LineNumberTable:
line 10: 0
line 11: 4
line 12: 12
line 13: 20
public static Singleton getInstance();
Code:
Stack=1, Locals=0, Args_size=0
0: getstatic #4; //Field instance:LSingleton;
3: areturn
LineNumberTable:
line 16: 0
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=2, Args_size=1
0: invokestatic #5; //Method getInstance:()LSingleton;
3: astore_1
4: getstatic #6; //Field java/lang/System.out:Ljava/io/PrintStream;
7: aload_1
8: pop
9: getstatic #2; //Field a:I
12: invokevirtual #7; //Method java/io/PrintStream.println:(I)V
15: getstatic #6; //Field java/lang/System.out:Ljava/io/PrintStream;
18: aload_1
19: pop
20: getstatic #3; //Field b:I
23: invokevirtual #7; //Method java/io/PrintStream.println:(I)V
26: return
LineNumberTable:
line 21: 0
line 22: 4
line 23: 15
line 24: 26
static {};
Code:
Stack=2, Locals=0, Args_size=0
0: new #8; //class Singleton
3: dup
4: invokespecial #9; //Method "<init>":()V
7: putstatic #4; //Field instance:LSingleton;
10: iconst_0
11: putstatic #3; //Field b:I
14: return
LineNumberTable:
line 4: 0
line 7: 10
}
constant开头的都是常量池中的内容,比如const #40 = Asciz Ljava/io/PrintStream这一行就是我们说的符号引用,在类加载的解析阶段会将一部分符号引用转化为直接引用,这里的直接引用对于类变量、类方法等来说是指向方法区的内存指针,对于类实例和实例变量等则是存储的偏移量。针对类加载的解析阶段,它有一个前提,方法在调用之前必须有一个可确定的调用版本,并且这个版本在运行期间不会改变。在java语言中满足“编译期确定,运行期不变”的方法有静态方法和私有方法两大类。
先来看下java中都有哪些类型的方法,构造方法,静态方法,私有方法,公有方法,final修饰的方法等等。实际上在JVM(jdk 1.6)层面只有四种方法调用的指令,分别是:
1.invokestatic调用静态方法
2.invokespecial调用类实例的构造器<init>方法,私有方法和父类方法
3.invokevirtual调用所有的虚方法
4.invokeinterface调用接口方法
只要能被invokestatic和invokespecial指令调用的方法,都可以在类加载的时候把符号引用解析为该方法的直接引用。这里主要是指,私有方法,静态方法,实例构造器,父类方法,这些方法统称为非虚方法。对应的当然也有虚方法,被invokevirtual和invokeinterface调用的则为虚方法,因为在编译期间并不能确定要调用的真正方法,所以称为虚方法。不过如果一个方法被final修饰即使被invokevirtual调用,也仍然是静态解析的。
众所周知java面向对象的三个重要特性,封装、继承、多态。而在jvm层面多态的实现由分派完成。分派有静态分派、动态分派。
静态分派典型的应用是方法重载,静态分派发生在编译阶段。编译过程中会根据变量的静态类型(比如A a = new B(),a为静态类型,B为实际类型)来确定方法的调用。对于方法参数的匹配也是根据变量的静态类型来确定,在很多情况下根据参数的类型并不能找到唯一的方法调用,这个时候的处理方式是找到一个最合适的方法。比如:
public class Test {
public static void main(String[] args) {
Test.print('a');
}
/*public static void print(int a)
{
System.out.println(a);
}*/
public static void print(long a)
{
System.out.println(a);
}
/* public static void print(char a)
{
}
*/}
此时Test.print('a')匹配到得方法为print(long a),如果将print(int a)的方法注释去掉,则会匹配到print(int a)这就是最合适的匹配。
再来看一下动态分派,动态分派的一个重要体现就是方法的重写,虽然父类引用可以指向子类对象,但是动态分派的方法调用是在运行时根据对象的实际类型去确认的。使用invokevirtual指令调用的动态分派会有一个查找过程:
1.找到操作数栈引用的实际对象类型,记作C
2.如果在类型C中找到与常量池中的描述和名称都相符的方法,则进行权限校验,如果通过返回方法的直接引用,否则返回异常。
3.否则,按照继承关系从下往上对C的各个父类执行第二步。
4.如果始终没有找到合适的方法,则抛出异常。
由于动态分配是非常频繁的动作,处于性能考虑,jvm在实现层面提供了一个叫做虚方法表的索引来提供性能,下面是书中的一张虚方法表结构图:
Father是父类son是子类,并且子类重写了父类的连个方法,hardChoice(QQ),hardChoice(_360),因此子类中的这两个方法指向了Son的类型数据,而这两个类都继承自Object且没重写它的任何方法,因此都指向了Object的类型数据。