虛擬機是相對於物理機的概念。 物理機的執行引擎是直接創建在處理器,緩存,指令集合操做系統底層上。 虛擬機的執行引擎是創建在軟件之上,不受物理條件限制,定製指令集與執行引擎。 虛擬機實現中,執行過程能夠是解釋執行和編譯執行,能夠單獨選擇,或者混合使用。 但全部虛擬機引擎從統一外觀(Facade)來講,都是輸入字節碼二進制流,字節碼解析執行,輸出執行結果。html
本章從概念角度講解虛擬機的方法調用和字節碼執行。java
Java虛擬機以方法做爲最基本的執行單元。每一個方法在執行時,都會有一個對應的棧幀(Stack Frame) .棧幀同時也是虛擬機棧的棧元素。棧幀存儲了方法的局部變量表、操做數棧、動態鏈接和方法返回地址等信息。 一個棧幀須要多大的局部變量表,須要多深的操做數棧,早在編譯成字節碼時就寫到了方發表的Code屬性中。 Code: stack=2, locals=1, args_size=1
所以一個棧幀須要分配多少內存,在運行前就已肯定,取決於源碼和虛擬機自身實現。 緩存
局部變量表容量最小單位爲變量槽(Variable Slot), 《Java虛擬機規範》規定一個變量槽能夠存放一個boolean,byte,char,init,float,reference或returnAddress類型的數據。32位系統能夠是32位,64位系統能夠是64位去實現一個變量槽。對於64位的數據類型(long和double),以高位對齊的方式分配兩個連續的變量槽。
複製代碼
因爲是線程私有,不管兩個連續變量槽的讀寫是否爲原子操做,都不會有線程安全問題。安全
當一個方法被調用時,會把參數值放到局部變量表中。類方法參數Slot從0開始。實例方法參數Slot從1開始,Slot0給了this,指向實例。
複製代碼
咱們比較類方法和實例方法的字節碼。markdown
public static int add(int a, int b) {return a + b;}
public int remove(int a, int b) {return a - b;}
複製代碼
public static int add(int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 a I
0 4 1 b I public int remove(int, int);
flags: ACC_PUBLIC
Code:
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this Lexecute/Reerer;
0 4 1 a I
0 4 2 b I
複製代碼
當變量的做用域小於整個方法體時,變量槽能夠複用,爲了節約棧內存空間。好比 {},if(){}等代碼塊內。變量槽複用會存在「輕微反作用」,內存回收問題。app
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
System.gc();
}
//執行結果
[GC (System.gc()) 69468K->66040K(251392K), 0.0007701 secs]
[Full GC (System.gc()) 66040K->65934K(251392K), 0.0040938 secs] //解釋: 雖然placeholder的做用域被限制,但gc時,局部變量表仍然引用placeholder,沒法被回收。 複製代碼
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
int i=0;
System.gc();
}
//執行結果
[GC (System.gc()) 69468K->66040K(251392K), 0.0007556 secs]
[Full GC (System.gc()) 66040K->398K(251392K), 0.0044978 secs] //解釋: 雖然placeholder的做用域被限制,int i=0複用了slot0,切斷了局部變量表的引用placeholder。 複製代碼
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
placeholder=null;
}
System.gc();
}
//執行結果
[GC (System.gc()) 69468K->66088K(251392K), 0.0022762 secs]
[Full GC (System.gc()) 66088K->398K(251392K), 0.0050265 secs] //解釋 主動釋放placeholder 複製代碼
類變量在準備階段,會被賦默認零值。而局部變量沒有準備階段。因此下面代碼是編譯不經過的,即使編譯經過,在檢驗階段,也會被發現,致使類加載失敗。ide
public static void fun4(){
int a;
//編譯失敗,Variable ‘a’ might not have been initialized
System.out.println(a);
}
複製代碼
操做數棧(Operand Stack) 字節碼指令讀取和寫入操做數棧。操做數棧中元素的數據類型必須與指令序列嚴格匹配。編譯階段和類檢驗階段都會去保證這個。 在大多數虛擬機實現中,上面棧幀的操做數棧與下面棧幀的局部變量會有一部分重疊,這樣不只節約了空間,重要的是在方法調用時直接公用數據,無須而外的參數複製。this
在類加載過程當中,會把符號引用解析爲直接引用。方法調用指令以常量池中的符號引用爲參數。這些方法符號引用一部分在類加載或者第一次使用時轉化爲直接引用,這種轉化被稱爲靜態解析。另一部分則須要在每次運行期間轉化爲直接引用,這部分稱之爲動態鏈接。spa
正常調用完成和異常調用完成。 恢復主調方法的執行狀態。操作系統
Java虛擬機中的5條方法調用指令:
方法按照類加載階段是否能轉化成直接引用分類,能夠分爲:
非虛方法的調用稱之爲解析(Resolution),"編譯器可知,運行期不可變",即類加載階段把符號引用轉化爲直接引用。 而另一個方法調用的方式稱之爲分派(Dispatch)。
分派(Dispatch)是靜態或者動態的,又或者是單分派或者多分派。重載或者重寫會出現同名方法。同名方法的選擇,我能夠稱之爲分派
Method Overload Resolution, 這部份內容實際上叫作方法重載解析。靜態分派發生在編譯階段。 先來看一段代碼,sayHello方法重載。
//方法靜態分派
public class StaticDispatch {
static abstract class Human{}
static class Man extends Human{}
static class Woman extends Human{}
public static void sayHello(Man man){System.out.println("hello,gentleman!"); }
public static void sayHello(Human guy){ System.out.println("hello,guy!");}
public static void sayHello(Woman women){System.out.println("hello,lady!");}
public static void main(String[] args) {
Human man=new Man();
Human woman=new Woman();
StaticDispatch dispatch=new StaticDispatch();
dispatch.sayHello(man);
dispatch.sayHello(woman);
}
}
//執行結果:
hello,guy!
hello,guy!
複製代碼
對應Class字節碼
public static void main(java.lang.String[]);
Code:
stack=2, locals=3, args_size=1
0: new #7 // class execute/StaticDispatch$Man
3: dup
4: invokespecial #8 // Method execute/StaticDispatch$Man."<init>":()V
7: astore_1
8: new #9 // class execute/StaticDispatch$Woman
11: dup
12: invokespecial #10 // Method execute/StaticDispatch$Woman."<init>":()V
15: astore_2
16: new #11 // class execute/StaticDispatch
19: dup
20: invokespecial #12 // Method "<init>":()V
23: astore_3
24: aload_3
25: aload_1
26: invokevirtual #13 // Method sayHello:(Lexecute/StaticDispatch$Human;)V
29: aload_3
30: aload_2
31: invokevirtual #13 // Method sayHello:(Lexecute/StaticDispatch$Human;)V
34: return
複製代碼
第0~15行,咱們構建了Man對象和Woman對象,並放入了局部變量表中。 第26行,執行方法Method sayHello:(Lexecute/StaticDispatch Human;)V, 實際執行的都是sayHello(Human)。而不是sayHello(Man)或者sayHello(Woman)。 這裏涉及到兩個類型:
編譯期並不知道對象的實際類型,因此按照對象的靜態類型去分派方法。
與重寫(Override)密切關聯。動態分派發生在運行期間。在運行時,肯定方法的接收者(方法所屬對象)
//方法動態分派
public class DynamicDispatch {
static abstract class Human {
public void sayHello() {System.out.println("hello,guy!");}
}
static class Man extends Human {
public void sayHello() {System.out.println("hello,gentleman!"); }
}
static class Woman extends Human {
public void sayHello() {System.out.println("hello,lady!");}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();//hello,gentleman!
woman.sayHello();//hello,lady!
man = new Woman();
man.sayHello();//hello,lady!
}
}
//執行結果:
hello,gentleman!
hello,lady!
hello,lady!
複製代碼
對應字節碼
0: new #2 // class execute/DynamicDispatch$Man
3: dup
4: invokespecial #3 // Method execute/DynamicDispatch$Man."<init>":()V
7: astore_1
8: new #4 // class execute/DynamicDispatch$Woman
11: dup
12: invokespecial #5 // Method execute/DynamicDispatch$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method execute/DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #6 // Method execute/DynamicDispatch$Human.sayHello:()V
24: new #4 // class execute/DynamicDispatch$Woman
27: dup
28: invokespecial #5 // Method execute/DynamicDispatch$Woman."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #6 // Method execute/DynamicDispatch$Human.sayHello:()V
36: return
複製代碼
第7行,astore_1存儲了Man對象 第15行,astore_2存儲了Woman對象 第16,17行,aload_1,invokevirtual.實際調用的是Man.sayHello()方法 第20,21行,aload_2,invokevirtual.實際調用的是Woman.sayHello()方法 第31行,astore_1存儲了Woman對象 第32,33行,aload_1,invokevirtual.實際調用的是Woman.sayHello()方法
運行期間,選擇是根據man和woman對象的實際類型分派方法。
小知識:字段永遠不參與多態,方法中訪問的屬性名始終是當前類的屬性。子類會遮蔽父類的同名字段
方法的宗量:方法的接收者與方法的參數 單分派:基於一種宗量分派 多分派:基於多種宗量分派。 當前Java語言是一門靜態多分派,動態單分派的語言。編譯期根據方法接收者和參數肯定方法的符號引用。運行期根據方法的接收者,解析和執行符號引用。
考慮下面一段代碼
public class Dispatch {
static class Father{
public void f() {System.out.println("father f void");}
public void f(int value) {System.out.println("father f int");}
}
static class Son extends Father{
public void f(int value) {System.out.println("Son f int"); }
public void f(char value) { System.out.println("Son f char");}
}
public static void main(String[] args) {
Father son=new Son();
son.f('a');
}
}
//執行結果: Son f int
複製代碼
字節碼
0: new #2 // class execute/Dispatch$Son
3: dup
4: invokespecial #3 // Method execute/Dispatch$Son."<init>":()V
7: astore_1
8: aload_1
9: bipush 97
11: invokevirtual #4 // Method execute/Dispatch$Father.f:(I)V
複製代碼
首先是編譯期的靜態分派,先選擇靜態類型Father,因爲Father中沒有f(char),則會選擇最合適的f(int),肯定方法爲Father.f:(I)V。其次是運行期,接收者爲Son,Son中有重寫的f:(I)V。因此最終執行的是Son.f:(I)V
虛方法表,接口方法表,類型繼承分析,守護內聯,內聯緩存
動態類型語言的關鍵特徵:類型檢查的主體過程是在運行期而不是編譯器,好比說Groovy、JavaScript、Lisp、Lua、Python。 靜態類型語言:編譯器就進行類型檢查,好比C++,Java。
Java虛擬機須要支持動態類型語言,因而在JDK7發佈 invokedynamic指令。
略
略
略
略
public int calc() {
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c;
}
複製代碼
stack=2, locals=4, args_size=1
0: bipush 100 //常量100壓入操做數棧頂
2: istore_1 //棧頂元素(100)存入變量槽1,同時消費掉棧頂元素
3: sipush 200 //常量200壓入操做數棧頂
6: istore_2 //棧頂元素(200)存入變量槽2,同時消費掉棧頂元素
7: sipush 300 //常量300壓入操做數棧頂
10: istore_3 //棧頂元素(300)存入變量槽3,同時消費掉棧頂元素
11: iload_1 //將局部變量slot1值100壓入操做數棧頂
12: iload_2 //將局部變量slot2值200壓入操做數棧頂
13: iadd //消費棧頂100和200,獲得300,並壓入棧頂
14: iload_3 //將局部變量slot3值300壓入操做數棧頂
15: imul //消費棧頂300和300,獲得90000,並壓入棧頂
16: ireturn //消費棧頂90000,整型結果返回給方法調用者
複製代碼