簡單說來,一個java程序的運行須要編輯源碼、編譯生成class文件、加載class文件、解釋或編譯運行class中的字節碼指令。java
下面有一段簡單的java源碼,經過它來看一下java程序的運行流程:eclipse
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,我直接就放到桌面了jvm
2.編譯生成class字節碼文件:編輯器
在桌面上,按住shift,而後按下鼠標右鍵:工具
點擊「在此處打開命令窗口」this
輸入命令javac Student.java將該源碼文件編譯生成.class字節碼文件。spa
因爲在源碼文件中定義了兩個類,一個接口,因此生成了3個.clsss文件:命令行
這樣能在java虛擬機上運行的字節碼文件就生成了3d
3.啓動java虛擬機運行字節碼文件:code
在命令行中輸入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_一、iload_二、iload_3這三條指令將slot一、slot二、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()返回,程序運行結束。
以上,就是一個簡單程序運行的大體過程,只是今天看書的一些理解,也許有錯誤的地方,但願不會貽笑大方……