Java程序運行時,必須經過編譯和運行兩個步驟。首先將后綴名為.java的源文件進行編譯,最終生成后綴名為.class的字節碼文件。然后Java虛擬機將編譯好的字節碼文件加載到內存(這個過程被稱為類加載,是由加載器完成的),然后虛擬機針對加載到內存的java類進行解釋執行,顯示結果。
Java的運行原理
在Java中引入了虛擬機的概念,即在機器和編譯程序之間加入了一層抽象的虛擬的機器。這台虛擬的機器在任何平台上都提供給編譯程序一個的共同的接口。編譯程序只需要面向虛擬機,生成虛擬機能夠理解的代碼,然后由解釋器來將虛擬機代碼轉換為特定系統的機器碼執行。在Java中,這種供虛擬機理解的代碼叫做字節碼(ByteCode),它不面向任何特定的處理器,只面向虛擬機。每一種平台的解釋器是不同的,但是實現的虛擬機是相同的。Java源程序經過編譯器編譯后變成字節碼,字節碼由虛擬機解釋執行,虛擬機將每一條要執行的字節碼送給解釋器,解釋器將其翻譯成特定機器上的機器碼,然后在特定的機器上運行。
Java代碼編譯和執行的整個過程
Java代碼編譯是由Java源碼編譯器來完成,流程圖如下所示:
Java字節碼的執行是由JVM執行引擎來完成,流程圖如下所示:
Java代碼編譯和執行的整個過程包含了以下三個重要的機制:
Java源碼編譯機制
類加載機制
類執行機制
Java源碼編譯機制
Java 源碼編譯由以下三個過程組成:(javac –verbose 輸出有關編譯器正在執行的操作的消息)
分析和輸入到符號表
注解處理
語義分析和生成class文件
最后生成的class文件由以下部分組成:
1、結構信息。包括class文件格式版本號及各部分的數量與大小的信息
2、元數據。對應於Java源碼中聲明與常量的信息。包含類/繼承的超類/實現的接口的聲明信息、域與方法聲明信息和常量池
3、方法信息。對應Java源碼中語句和表達式對應的信息。包含字節碼、異常處理器表、求值棧與局部變量區大小、求值棧的類型記錄、調試符號信息
類加載機制
JVM的類加載是通過ClassLoader及其子類來完成的,類的層次關系和加載順序可以由下圖來描述:
1)Bootstrap ClassLoader /啟動類加載器
$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++實現,不是ClassLoader子類
2)Extension ClassLoader/擴展類加載器
負責加載java平台中擴展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包
3)App ClassLoader/ 系統類加載器
負責記載classpath中指定的jar包及目錄中class
4)Custom ClassLoader/用戶自定義類加載器(java.lang.ClassLoader的子類)
屬於應用程序根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規范自行實現ClassLoader
加載過程中會先檢查類是否被已加載,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,只要某個classloader已加載就視為已加載此類,保證此類只所有ClassLoader加載一次。而加載的順序是自頂向下,也就是由上層來逐層嘗試加載此類。
類加載雙親委派機制介紹和分析
在這里,需要着重說明的是,JVM在加載類時默認采用的是雙親委派機制。通俗的講,就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委托給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。
類執行機制
JVM是基於棧的體系結構來執行class字節碼的。線程創建后,都會產生程序計數器(PC)和棧(Stack),程序計數器存放下一條要執行的指令在方法內的偏移量,棧中存放一個個棧幀,每個棧幀對應着每個方法的每次調用,而棧幀又是有局部變量區和操作數棧兩部分組成,局部變量區用於存放方法中的局部變量和參數,操作數棧中用於存放方法執行過程中產生的中間結果。
public static void main(String[] args){
Student s = new Student(23,“dqrcsc”,“20150723”);//執行完畢
s.study(5,6);
Student.getCnt();
s.run();
}
執行過程如下
1、為main方法創建棧幀:
局部變量表長度為2,slot0存放參數args,slot1存放局部變量Student s,操作數棧最大深度為5。
2、new#7指令,在java堆中創建一個Student對象,並將其引用值放入棧頂。
3、初始化一個對象(通過實例構造的方式)
up指令:復制棧頂的值,然后將復制的結果入棧。
bipush 23:將單字節常量值23入棧。
ldc #8:將#8這個常量池中的常量即”dqrcsc”取出,並入棧。
ldc #9:將#9這個常量池中的常量即”20150723”取出,並入棧。
4、invokespecial #10:調用#10這個常量所代表的方法,即Student.()這個方法,這步是為了初始化對象s的各項值
《init》()方法,是編譯器將調用父類的《init》()的語句、構造代碼塊、實例字段賦值語句,以及自己編寫的構造方法中的語句整合在一起生成的一個方法。保證調用父類的《init》()方法在最開頭,自己編寫的構造方法語句在最后,而構造代碼塊及實例字段賦值語句按出現的順序按序整合到《init》()方法中。
注意到Student.《init》()方法的最大操作數棧深度為3,局部變量表大小為4。
此時需注意:從dup到ldc #9這四條指令向棧中添加了4個數據,而Student.()方法剛好也需要4個參數:
public Student(int age, String name, String sid){
super(age,name);
this.sid = sid;
}
雖然定義中只顯式地定義了傳入3個參數,而實際上會隱含傳入一個當前對象的引用作為第一個參數,所以四個參數依次為this,age,name,sid。
上面的4條指令剛好把這四個參數的值依次入棧,進行參數傳遞,然后調用了Student.《init》()方法,會創建該方法的棧幀,並入棧。棧幀中的局部變量表的第0到4個slot分別保存着入棧的那四個參數值。
創建Studet.《init》()方法的棧幀:
Student.《init》()方法中的字節碼指令:
aload_0:將局部變量表slot0處的引用值入棧
aload_1:將局部變量表slot1處的int值入棧
aload_2:將局部變量表slot2處的引用值入棧
invokespecial #1:調用Person.()方法,同調用Student.過程類似,創建棧幀,將三個參數的值存放到局部變量表等,這里就不畫圖了……
從Person.()返回之后,用於傳參的棧頂的3個值被回收了。
aload_0:將slot0處的引用值入棧。
aload_3:將slot3處的引用值入棧。
putfield #2:將當前棧頂的值”20150723”賦值給0x2222所引用對象的sid字段,然后棧中的兩個值出棧。
return:返回調用方即main()方法,當前方法棧幀出棧。
重新回到main()方法中,繼續執行下面的字節碼指令:
astore_1:將當前棧頂引用類型的值賦值給slot1處的局部變量,然后出棧。
5、到這兒為止,第一行代碼執行完畢,將s返回給局部變量表,執行下邊的
aload_1:slot1處的引用類型的值入棧
iconst_5:將常數5入棧,int型常數只有0-5有對應的iconst_x指令
bipush 6:將常數6入棧
6、開始執行第二行代碼,也就是strudy方法
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;
}123456789
bipush 10:將10入棧
istore_3:將棧頂的10賦值給slot3處的int局部變量,即c,出棧。
bipush 20:將20入棧
istore 4:將棧頂的20付給slot4處的int局部變量,即d,出棧。
上面4條指令,完成對c和d的賦值工作。
iload_1、iload_2、iload_3這三條指令將slot1、slot2、slot3這三個局部變量入棧:
imul:將棧頂的兩個值出棧,相乘的結果入棧:
iadd:將當前棧頂的兩個值出棧,相加的結果入棧
iload 4:將slot4處的int型的局部變量入
isub:將棧頂兩個值出棧,相減結果入棧:
ireturn:將當前棧頂的值返回到調用方。
7、到這兒為止,第二行代碼執行完畢,返回值返回給s,執行下邊的
public static void main(String[] args){
Student s = new Student(23,“dqrcsc”,“20150723”);//執行完畢
s.study(5,6);
Student.getCnt();
s.run();
}1234567891011
invokestatic #12 調用靜態方法getCnt()不需要傳任何參數
pop:getCnt()方法有返回值,將其出棧
aload_1:將slot1處的引用值入棧
invokevirtual #13:調用0x2222對象的run()方法,重寫自父類的方法,需要動態分派,所以使用invokevirtual指令
return:main()返回,程序運行結束。