JVM中方法调用的实现

我们写代码时方法调用是最常见的场景,但是这种最常见的场景在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的类型数据。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章