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的類型數據。


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