我們寫代碼時方法調用是最常見的場景,但是這種最常見的場景在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的類型數據。