簡單說來,一個java程序的運行需要編輯源碼、編譯生成class文件、加載class文件、解釋或編譯運行class中的字節碼指令。
下面有一段簡單的java源碼,通過它來看一下java程序的運行流程:
1 class Person 2 3 { 4 5 private String name; 6 7 private int age; 8 9 10 11 public Person(int age, String name){ 12 13 this.age = age; 14 15 this.name = name; 16 17 } 18 19 public void run(){ 20 21 22 23 } 24 25 } 26 27 28 29 interface IStudyable 30 31 { 32 33 public int study(int a, int b); 34 35 } 36 37 public class Student extends Person implements IStudyable 38 39 { 40 41 private static int cnt=5; 42 43 static{ 44 45 cnt++; 46 47 } 48 49 private String sid; 50 51 public Student(int age, String name, String sid){ 52 53 super(age,name); 54 55 this.sid = sid; 56 57 } 58 59 public void run(){ 60 61 System.out.println("run()..."); 62 63 } 64 65 public int study(int a, int b){ 66 67 int c = 10; 68 69 int d = 20; 70 71 return a+b*c-d; 72 73 } 74 75 public static int getCnt(){ 76 77 return cnt; 78 79 } 80 81 public static void main(String[] args){ 82 83 Student s = new Student(23,"dqrcsc","20150723"); 84 85 s.study(5,6); 86 87 Student.getCnt(); 88 89 s.run(); 90 91 } 92 93 }
1.編輯源碼:無論是使用記事本還是別的什么,編寫上面的代碼,然后保存到Student.java,我直接就放到桌面了
2.編譯生成class字節碼文件:
在桌面上,按住shift,然后按下鼠標右鍵:
點擊“在此處打開命令窗口”
輸入命令javac Student.java將該源碼文件編譯生成.class字節碼文件。
由於在源碼文件中定義了兩個類,一個接口,所以生成了3個.clsss文件:
這樣能在java虛擬機上運行的字節碼文件就生成了
3.啟動java虛擬機運行字節碼文件:
在命令行中輸入java Student這個命令,就啟動了一個java虛擬機,然后加載Student.class字節碼文件到內存,然后運行內存中的字節碼指令了。
我們從編譯到運行java程序,只輸入了兩個命令,甚至,如果使用集成開發環境,如eclipse,只要ctrl+s保存就完成了增量編譯,只需要按下一個按鈕就運行了java程序。但是,在這些簡單操作的背后還有一些操作……
1.從源碼到字節碼:
字節碼文件,看似很微不足道的東西,卻真正實現了java語言的跨平台。各種不同平台的虛擬機都統一使用這種相同的程序存儲格式。更進一步說,jvm運行的是class字節碼文件,只要是這種格式的文件就行,所以,實際上jvm並不像我之前想象地那樣與java語言緊緊地捆綁在一起。如果非常熟悉字節碼的格式要求,可以使用二進制編輯器自己寫一個符合要求的字節碼文件,然后交給jvm去運行;或者把其他語言編寫的源碼編譯成字節碼文件,交給jvm去運行,只要是合法的字節碼文件,jvm都會正確地跑起來。所以,它還實現了跨語言……
通過jClassLib可以直接查看一個.class文件中的內容,也可以給JDK中的javap命令指定參數,來查看.class文件的相關信息:
javap –v Student
好多輸出,在命令行窗口查看不是太方便,可以輸出重定向下:
javap –v Student > 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的功能模塊主要包括類加載器、執行引擎和垃圾回收系統。
3.類加載器加載Student.class到內存:
1)類加載器會在指定的classpath中找到Student.class這個文件,然后讀取字節流中的數據,將其存儲在方法區中。
2)會根據Student.class的信息建立一個Class對象,這個對象比較特殊,一般也存放在方法區中,用於作為運行時訪問Student類的各種數據的接口。
3)必要的驗證工作,格式、語義等
4)為Student中的靜態字段分配內存空間,也是在方法區中,並進行零初始化,即數字類型初始化為0,boolean初始化為false,引用類型初始化為null等。
在Student.java中只有一個靜態字段:
private static int cnt=5;
此時,並不會執行賦值為5的操作,而是將其初始化為0。
5)由於已經加載到內存了,所以原來字節碼文件中存放的部分方法、字段等的符號引用可以解析為其在內存中的直接引用了,而不一定非要等到真正運行時才進行解析。
6)在編譯階段,編譯器收集所有的靜態字段的賦值語句及靜態代碼塊,並按語句出現的順序拼接出一個類初始化方法<clinit>()。此時,執行引擎會調用這個方法對靜態字段進行代碼中編寫的初始化操作。
在Student.java中關於靜態字段的賦值及靜態代碼塊有兩處:
1 private static int cnt=5; 2 3 static{ 4 5 cnt++; 6 7 }
將按出現順序拼接,形式如下:
1 void <clinit>(){ 2 3 cnt = 5; 4 5 cnt++; 6 7 }
可以通過jClassLib這個工具看到生成的<clinit>()方法的字節碼指令:
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:+TraceClassLoading Student,信息太多,可以重定向下:
查看輸出的loadClass.txt文件:
可以看到最先加載的是Object.class這個類,當然了,所有類的父類。
直到第390行才看到自己定義的部分被加載,先是Student實現的接口IStudyable,然后是其父類Person,然后才是Student自身,然后是一個啟動類的加載,然后就是找到main()方法,執行了。
4.執行引擎找到main()這個入口方法,執行其中的字節碼指令:
要了解方法的運行,需要先稍微了解下java棧:
JVM中通過java棧,保存方法調用運行的相關信息,每當調用一個方法,會根據該方法的在字節碼中的信息為該方法創建棧幀,不同的方法,其棧幀的大小有所不同。棧幀中的內存空間還可以分為3塊,分別存放不同的數據:
局部變量表:存放該方法調用者所傳入的參數,及在該方法的方法體中創建的局部變量。
操作數棧:用於存放操作數及計算的中間結果等。
其他棧幀信息:如返回地址、當前方法的引用等。
只有當前正在運行的方法的棧幀位於棧頂,當前方法返回,則當前方法對應的棧幀出棧,當前方法的調用者的棧幀變為棧頂;當前方法的方法體中若是調用了其他方法,則為被調用的方法創建棧幀,並將其壓入棧頂。
注意:局部變量表及操作數棧的最大深度在編譯期間就已經確定了,存儲在該方法字節碼的Code屬性中。
簡單查看Student.main()的運行過程:
簡單看下main()方法:
1 public static void main(String[] args){ 2 3 Student s = new Student(23,"dqrcsc","20150723"); 4 5 s.study(5,6); 6 7 Student.getCnt(); 8 9 s.run(); 10 11 }
對應的字節碼,兩者對照着看起來更易於理解些:
注意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.<init>()這個方法
<init>()方法,是編譯器將調用父類的<init>()的語句、構造代碼塊、實例字段賦值語句,以及自己編寫的構造方法中的語句整合在一起生成的一個方法。保證調用父類的<init>()方法在最開頭,自己編寫的構造方法語句在最后,而構造代碼塊及實例字段賦值語句按出現的順序按序整合到<init>()方法中。
注意到Student.<init>()方法的最大操作數棧深度為3,局部變量表大小為4。
此時需注意:從dup到ldc #9這四條指令向棧中添加了4個數據,而Student.<init>()方法剛好也需要4個參數:
1 public Student(int age, String name, String sid){ 2 3 super(age,name); 4 5 this.sid = sid; 6 7 }
雖然定義中只顯式地定義了傳入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.<init>()方法,同調用Student.<init>過程類似,創建棧幀,將三個參數的值存放到局部變量表等,這里就不畫圖了……
從Person.<init>()返回之后,用於傳參的棧頂的3個值被回收了。
aload_0:將slot0處的引用值入棧。
aload_3:將slot3處的引用值入棧。
putfield #2:將當前棧頂的值”20150723”賦值給0x2222所引用對象的sid字段,然后棧中的兩個值出棧。
return:返回調用方,即main()方法,當前方法棧幀出棧。
重新回到main()方法中,繼續執行下面的字節碼指令:
astore_1:將當前棧頂引用類型的值賦值給slot1處的局部變量,然后出棧。
aload_1:slot1處的引用類型的值入棧
iconst_5:將常數5入棧,int型常數只有0-5有對應的iconst_x指令
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的賦值工作。
iload_1、iload_2、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()返回,程序運行結束。
以上,就是一個簡單程序運行的大致過程,只是今天看書的一些理解,也許有錯誤的地方,希望不會貽笑大方……