點選上方“Java技術驛站”,選擇“置頂公眾號”。
有內涵、有價值的文章第一時間送達!
精品專欄
作者:某人的喵星人
原文:https://www.cnblogs.com/dqrcsc/p/4671879.html
簡單說來,一個java程式的執行需要編輯原始碼、編譯生成class檔案、載入class檔案、解釋或編譯執行class中的位元組碼指令。
下麵有一段簡單的java原始碼,透過它來看一下java程式的執行流程:
class Person{
private String name;
private int age;
public Person(int age, String name){
this.age = age;
this.name = name;
}
public void run(){
}
}
interface IStudyable{
public int study(int a, int b);
}
public class Student extends Person implements IStudyable{
private static int cnt=5;
static{
cnt++;
}
private String sid;
public Student(int age, String name, String sid){
super(age,name);
this.sid = sid;
}
public void run(){
System.out.println("run()...");
}
public int study(int a, int b){
int c = 10;
int d = 20;
return a+b*c-d;
}
public static int getCnt(){
return cnt;
}
public static void main(String[] args){
Student s = new Student(23,"dqrcsc","20150723");
s.study(5,6);
Student.getCnt();
s.run();
}
}
1、編輯原始碼
無論是使用記事本還是別的什麼,編寫上面的程式碼,然後儲存到Student.java,我直接就放到桌面了
2.編譯生成class位元組碼檔案
開啟命令視窗,輸入命令javac Student.java將該原始碼檔案編譯生成.class位元組碼檔案。
由於在原始碼檔案中定義了兩個類,一個介面,所以生成了3個.clsss檔案:
這樣能在java虛擬機器上執行的位元組碼檔案就生成了
啟動java虛擬機器執行位元組碼檔案
在命令列中輸入 javaStudent
這個命令,就啟動了一個 java 虛擬機器,然後載入 Student.class 位元組碼檔案到記憶體,然後執行記憶體中的位元組碼指令了。
我們從編譯到執行 java 程式,只輸入了兩個命令,甚至,如果使用整合開發環境,如 eclipse,只要 ctrl+s 儲存就完成了增量編譯,只需要按下一個按鈕就運行了 java 程式。但是,在這些簡單操作的背後還有一些操作……
從原始碼到位元組碼
位元組碼檔案,看似很微不足道的東西,卻真正實現了 java 語言的跨平臺。各種不同平臺的虛擬機器都統一使用這種相同的程式儲存格式。更進一步說,jvm 執行的是 class 位元組碼檔案,只要是這種格式的檔案就行,所以,實際上 jvm 並不像我之前想象地那樣與 java 語言緊緊地捆綁在一起。如果非常熟悉位元組碼的格式要求,可以使用二進位制編輯器自己寫一個符合要求的位元組碼檔案,然後交給 jvm 去執行;或者把其他語言編寫的原始碼編譯成位元組碼檔案,交給 jvm 去執行,只要是合法的位元組碼檔案, jvm 都會正確地跑起來。所以,它還實現了跨語言……
透過 jClassLib 可以直接檢視一個 .class 檔案中的內容,也可以給 JDK 中的 javap 命令指定引數,來檢視 .class 檔案的相關資訊:
javap–vStudent
好多輸出,在命令列視窗檢視不是太方便,可以輸出重定向下:
javap–vStudent>Student.class.txt
桌面上多出了一個 Student.class.txt
檔案,裡面存放著便於閱讀的Student.class檔案中相關的資訊
裡面的內容如下(部分):
部分 class 檔案內容,從上面圖中,可以看到這些資訊來自於 Student.class ,編譯自 Student.java ,編譯器的主版本號是 52,也就是 jdk1.8,這個類是 public ,然後是存放類中常量的常量池,各個方法的位元組碼等,這裡就不一一記錄了。
總之,我想說的就是位元組碼檔案很簡單很強大,它存放了這個類的各種資訊:欄位、方法、父類、實現的介面等各種資訊。
2.Java 虛擬機器的基本結構及其記憶體分割槽
Java 虛擬機器要執行位元組碼指令,就要先載入位元組碼檔案,誰來載入,怎麼載入,載入到哪裡……誰來執行,怎麼執行,同樣也要考慮……
上面是一個JVM的基本結構及記憶體分割槽的圖,有點抽象,簡單說明下:
JVM中把記憶體分為直接記憶體、方法區、Java棧、Java堆、本地方法棧、PC暫存器等。
-
直接記憶體:就是原始的記憶體區
-
方法區:用於存放類、介面的元資料資訊,載入進來的位元組碼資料都儲存在方法區
-
Java棧:執行引擎執行位元組碼時的執行時記憶體區,採用棧幀的形式儲存每個方法的呼叫執行資料
-
本地方法棧:執行引擎呼叫本地方法時的執行時記憶體區
-
Java堆:執行時資料區,各種物件一般都儲存在堆上
-
PC暫存器:功能如同CPU中的PC暫存器,指示要執行的位元組碼指令。
JVM的功能模組主要包括類載入器、執行引擎和垃圾回收系統。
類載入器載入 Student.class 到記憶體
-
類載入器會在指定的 classpath 中找到 Student.class 這個檔案,然後讀取位元組流中的資料,將其儲存在方法區中。
-
會根據 Student.class 的資訊建立一個 Class 物件,這個物件比較特殊,一般也存放在方法區中,用於作為執行時訪問 Student 類的各種資料的介面。
-
必要的驗證工作,格式、語意等
-
為 Student 中的靜態欄位分配記憶體空間,也是在方法區中,併進行零初始化,即數字型別初始化為 0 ,boolean 初始化為 false,取用型別初始化為 null 等。在 Student.java 中只有一個靜態欄位:
privatestaticintcnt=5;
此時,並不會執行賦值為5的操作,而是將其初始化為0。 -
由於已經載入到記憶體了,所以原來位元組碼檔案中存放的部分方法、欄位等的符號取用可以解析為其在記憶體中的直接取用了,而不一定非要等到真正執行時才進行解析。
-
在編譯階段,編譯器收集所有的靜態欄位的賦值陳述句及靜態程式碼塊,並按陳述句出現的順序拼接出一個類初始化方法
。此時,執行引擎會呼叫這個方法對靜態欄位進行程式碼中編寫的初始化操作。()
在 Student.java 中關於靜態欄位的賦值及靜態程式碼塊有兩處:
private static int cnt=5;
static{
cnt++;
}
將按出現順序拼接,形式如下:
void
(){
cnt = 5;
cnt++;
}
可以透過 jClassLib 這個工具看到生成的
方法的位元組碼指令:
-
iconst_5 :指令把常數5入棧
-
putstatic #6:將棧頂的5賦值給 Student.cnt 這個靜態欄位
-
getstatic #6:獲取Student.cnt這個靜態欄位的值,並將其放入棧頂
-
iconst_1:把常數1入棧
-
iadd:取出棧頂的兩個整數,相加,結果入棧
-
putstatic #6:取出棧頂的整數,賦值給Student.cnt
-
return:從當前方法中傳回,沒有任何傳回值。
從位元組碼來看,確實先後執行了 cnt=5
及 cnt++
這兩行程式碼。
在這裡有一點要註意的是,這裡籠統的描述了下類的載入及初始化過程,但是,實際中,有可能只進行了類載入,而沒有進行初始化工作,原因就是在程式中並沒有訪問到該類的欄位及方法等。
此外,實際載入過程也會相對來說比較複雜,一個類載入之前要載入它的父類及其實現的介面:載入的過程可以透過java –XX:+TraceClassLoading引數檢視:
如: java-XX:+TraceClassLoadingStudent
,資訊太多,可以重定向下:
檢視輸出的 loadClass.txt 檔案:
可以看到最先載入的是 Object.class 這個類,當然了,所有類的父類。
直到第 390 行才看到自己定義的部分被載入,先是 Studen t實現的介面 IStudyable ,然後是其父類 Person ,然後才是 Student 自身,然後是一個啟動類的載入,然後就是找到 main() 方法,執行了。
執行引擎找到 main()
要瞭解方法的執行,需要先稍微瞭解下 java 棧:
JVM 中透過 java 棧,儲存方法呼叫執行的相關資訊,每當呼叫一個方法,會根據該方法的在位元組碼中的資訊為該方法建立棧幀,不同的方法,其棧幀的大小有所不同。棧幀中的記憶體空間還可以分為3塊,分別存放不同的資料:
-
區域性變數表:存放該方法呼叫者所傳入的引數,及在該方法的方法體中建立的區域性變數。
-
運算元棧:用於存放運算元及計算的中間結果等。
-
其他棧幀資訊:如傳回地址、當前方法的取用等。
只有當前正在執行的方法的棧幀位於棧頂,當前方法傳回,則當前方法對應的棧幀出棧,當前方法的呼叫者的棧幀變為棧頂;當前方法的方法體中若是呼叫了其他方法,則為被呼叫的方法建立棧幀,並將其壓入棧頂。
註意:區域性變數表及運算元棧的最大深度在編譯期間就已經確定了,儲存在該方法位元組碼的Code屬性中。
簡單檢視 Student.main() 的執行過程
簡單看下main()方法:
public static void main(String[] args){
Student s = new Student(23,"dqrcsc","20150723");
s.study(5,6);
Student.getCnt();
s.run();
}
對應的位元組碼,兩者對照著看起來更易於理解些:
註意main()方法的這幾個資訊:
-
Mximum stack depth:指定當前方法即 main() 方法對應棧幀中的運算元棧的最大深度,當前值為5
-
Maximum local variables:指定main()方法中區域性變數表的大小,當前為2,及有兩個slot用於存放方法的引數及區域性變數。
-
Code length:指定main()方法中程式碼的長度。
開始模擬main()中一條條位元組碼指令的執行:
建立棧幀:
區域性變數表長度為 2,slot0 存放引數 args ,slot1 存放區域性變數 Student s,運算元棧最大深度為 5。
new #7 指令:在 java 堆中建立一個 Student 物件,並將其取用值放入棧頂。
-
dup指令:複製棧頂的值,然後將複製的結果入棧。
-
bipush 23:將單位元組常量值23入棧。
-
ldc #8:將#8這個常量池中的常量即”dqrcsc”取出,併入棧。
-
ldc #9:將#9這個常量池中的常量即”20150723”取出,併入棧。
invokespecial #10:呼叫#10這個常量所代表的方法,即Student.
方法,是編譯器將呼叫父類的
的陳述句、構造程式碼塊、實體欄位賦值陳述句,以及自己編寫的構造方法中的陳述句整合在一起生成的一個方法。保證呼叫父類的
方法在最開頭,自己編寫的構造方法陳述句在最後,而構造程式碼塊及實體欄位賦值陳述句按出現的順序按序整合到
方法中。
註意到 Student.<init>()
方法的最大運算元棧深度為 3,區域性變數表大小為 4。
此時需註意:從 dup 到 ldc #9 這四條指令向棧中添加了4個資料,而Student.
public Student(int age, String name, String sid){
super(age,name);
this.sid = sid;
}
雖然定義中只顯式地定義了傳入3個引數,而實際上會隱含傳入一個當前物件的取用作為第一個引數,所以四個引數依次為this,age,name,sid。
上面的4條指令剛好把這四個引數的值依次入棧,進行引數傳遞,然後呼叫了Student.
建立 Studet.<init>()
方法的棧幀:
Student.
-
aload_0:將區域性變數表slot0處的取用值入棧
-
aload_1:將區域性變數表slot1處的int值入棧
-
aload_2:將區域性變數表slot2處的取用值入棧
-
invokespecial #1:呼叫Person.
()方法,同呼叫Student. 過程類似,建立棧幀,將三個引數的值存放到區域性變數表等,這裡就不畫圖了……
從Person.
-
aload_0:將slot0處的取用值入棧。
-
aload_3:將slot3處的取用值入棧。
-
putfield #2:將當前棧頂的值”20150723”賦值給0x2222所取用物件的sid欄位,然後棧中的兩個值出棧。
-
return:傳回呼叫方,即main()方法,當前方法棧幀出棧。
重新回到main()方法中,繼續執行下麵的位元組碼指令:
astore_1:將當前棧頂取用型別的值賦值給slot1處的區域性變數,然後出棧。
-
aload_1:slot1處的取用型別的值入棧
-
iconst5:將常數5入棧,int型常數只有0-5有對應的iconstx指令
-
bipush 6:將常數6入棧
-
invokevirtual #11:呼叫虛方法study(),這個方法是重寫的介面中的方法,需要動態分派,所以使用了invokevirtual指令。
建立study()方法的棧幀:
最大棧深度3,區域性變數表5
方法的java原始碼:
public int study(int a, int b){
int c = 10;
int d = 20;
return a+b*c-d;
}
對應的位元組碼:
註意到這裡,透過 jClassLib 工具檢視的位元組碼指令有點問題,與原始碼有偏差……
改用透過命令 javap –v Student 檢視 study() 的位元組碼指令:
-
bipush 10:將10入棧
-
istore_3:將棧頂的10賦值給slot3處的int區域性變數,即c,出棧。
-
bipush 20:將20入棧
-
istore 4:將棧頂的20付給slot4處的int區域性變數,即d,出棧。
上面4條指令,完成對c和d的賦值工作。
iload1、iload2、iload_3這三條指令將slot1、slot2、slot3這三個區域性變數入棧:
-
imul:將棧頂的兩個值出棧,相乘的結果入棧:
-
iadd:將當前棧頂的兩個值出棧,相加的結果入棧
-
iload 4:將slot4處的int型的區域性變數入
-
isub:將棧頂兩個值出棧,相減結果入棧:
-
ireturn:將當前棧頂的值傳回到呼叫方。
重新回到main()方法中:
-
pop指令,將study()方法的傳回值出棧
-
invokestatic #12 呼叫靜態方法getCnt()不需要傳任何引數
-
pop:getCnt()方法有傳回值,將其出棧
-
aload_1:將slot1處的取用值入棧
-
invokevirtual #13:呼叫0x2222物件的run()方法,重寫自父類的方法,需要動態分派,所以使用invokevirtual指令
-
return:main()傳回,程式執行結束。
以上,就是一個簡單程式執行的大致過程