簡(jiǎn)單說(shuō)來(lái),一個(gè)java程序的運(yùn)行需要編輯源碼,、編譯生成class文件,、加載class文件、解釋或編譯運(yùn)行class中的字節(jié)碼指令,。 下面有一段簡(jiǎn)單的java源碼,,通過(guò)它來(lái)看一下java程序的運(yùn)行流程: 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.編輯源碼:無(wú)論是使用記事本還是別的什么,編寫上面的代碼,,然后保存到Student.java,,我直接就放到桌面了
2.編譯生成class字節(jié)碼文件: 在桌面上,,按住shift,,然后按下鼠標(biāo)右鍵:
點(diǎn)擊“在此處打開命令窗口”
輸入命令javac Student.java將該源碼文件編譯生成.class字節(jié)碼文件。 由于在源碼文件中定義了兩個(gè)類,,一個(gè)接口,,所以生成了3個(gè).clsss文件:
這樣能在java虛擬機(jī)上運(yùn)行的字節(jié)碼文件就生成了 3.啟動(dòng)java虛擬機(jī)運(yùn)行字節(jié)碼文件:
在命令行中輸入java Student這個(gè)命令,就啟動(dòng)了一個(gè)java虛擬機(jī),,然后加載Student.class字節(jié)碼文件到內(nèi)存,,然后運(yùn)行內(nèi)存中的字節(jié)碼指令了。
我們從編譯到運(yùn)行java程序,,只輸入了兩個(gè)命令,,甚至,如果使用集成開發(fā)環(huán)境,如eclipse,,只要ctrl+s保存就完成了增量編譯,,只需要按下一個(gè)按鈕就運(yùn)行了java程序。但是,,在這些簡(jiǎn)單操作的背后還有一些操作……
1.從源碼到字節(jié)碼: 字節(jié)碼文件,,看似很微不足道的東西,卻真正實(shí)現(xiàn)了java語(yǔ)言的跨平臺(tái),。各種不同平臺(tái)的虛擬機(jī)都統(tǒng)一使用這種相同的程序存儲(chǔ)格式,。更進(jìn)一步說(shuō),jvm運(yùn)行的是class字節(jié)碼文件,,只要是這種格式的文件就行,,所以,實(shí)際上jvm并不像我之前想象地那樣與java語(yǔ)言緊緊地捆綁在一起,。如果非常熟悉字節(jié)碼的格式要求,,可以使用二進(jìn)制編輯器自己寫一個(gè)符合要求的字節(jié)碼文件,然后交給jvm去運(yùn)行,;或者把其他語(yǔ)言編寫的源碼編譯成字節(jié)碼文件,,交給jvm去運(yùn)行,只要是合法的字節(jié)碼文件,,jvm都會(huì)正確地跑起來(lái),。所以,它還實(shí)現(xiàn)了跨語(yǔ)言…… 通過(guò)jClassLib可以直接查看一個(gè).class文件中的內(nèi)容,,也可以給JDK中的javap命令指定參數(shù),,來(lái)查看.class文件的相關(guān)信息: javap –v Student
好多輸出,在命令行窗口查看不是太方便,,可以輸出重定向下: javap –v Student > Student.class.txt
桌面上多出了一個(gè)Student.class.txt文件,,里面存放著便于閱讀的Student.class文件中相關(guān)的信息
部分class文件內(nèi)容,從上面圖中,,可以看到這些信息來(lái)自于Student.class,,編譯自Student.java,編譯器的主版本號(hào)是52,,也就是jdk1.8,,這個(gè)類是public,然后是存放類中常量的常量池,,各個(gè)方法的字節(jié)碼等,,這里就不一一記錄了。 總之,,我想說(shuō)的就是字節(jié)碼文件很簡(jiǎn)單很強(qiáng)大,,它存放了這個(gè)類的各種信息:字段,、方法、父類,、實(shí)現(xiàn)的接口等各種信息,。
2.Java虛擬機(jī)的基本結(jié)構(gòu)及其內(nèi)存分區(qū): Java虛擬機(jī)要運(yùn)行字節(jié)碼指令,就要先加載字節(jié)碼文件,,誰(shuí)來(lái)加載,,怎么加載,加載到哪里……誰(shuí)來(lái)運(yùn)行,,怎么運(yùn)行,,同樣也要考慮……
上面是一個(gè)JVM的基本結(jié)構(gòu)及內(nèi)存分區(qū)的圖,有點(diǎn)抽象,,有點(diǎn)丑……簡(jiǎn)單說(shuō)明下: JVM中把內(nèi)存分為直接內(nèi)存,、方法區(qū)、Java棧,、Java堆,、本地方法棧、PC寄存器等,。 直接內(nèi)存:就是原始的內(nèi)存區(qū) 方法區(qū):用于存放類,、接口的元數(shù)據(jù)信息,加載進(jìn)來(lái)的字節(jié)碼數(shù)據(jù)都存儲(chǔ)在方法區(qū) Java棧:執(zhí)行引擎運(yùn)行字節(jié)碼時(shí)的運(yùn)行時(shí)內(nèi)存區(qū),,采用棧幀的形式保存每個(gè)方法的調(diào)用運(yùn)行數(shù)據(jù) 本地方法棧:執(zhí)行引擎調(diào)用本地方法時(shí)的運(yùn)行時(shí)內(nèi)存區(qū) Java堆:運(yùn)行時(shí)數(shù)據(jù)區(qū),,各種對(duì)象一般都存儲(chǔ)在堆上 PC寄存器:功能如同CPU中的PC寄存器,指示要執(zhí)行的字節(jié)碼指令,。 JVM的功能模塊主要包括類加載器,、執(zhí)行引擎和垃圾回收系統(tǒng)。 3.類加載器加載Student.class到內(nèi)存: 1)類加載器會(huì)在指定的classpath中找到Student.class這個(gè)文件,,然后讀取字節(jié)流中的數(shù)據(jù),,將其存儲(chǔ)在方法區(qū)中。 2)會(huì)根據(jù)Student.class的信息建立一個(gè)Class對(duì)象,,這個(gè)對(duì)象比較特殊,,一般也存放在方法區(qū)中,用于作為運(yùn)行時(shí)訪問(wèn)Student類的各種數(shù)據(jù)的接口,。 3)必要的驗(yàn)證工作,,格式、語(yǔ)義等 4)為Student中的靜態(tài)字段分配內(nèi)存空間,,也是在方法區(qū)中,并進(jìn)行零初始化,,即數(shù)字類型初始化為0,,boolean初始化為false,,引用類型初始化為null等。 在Student.java中只有一個(gè)靜態(tài)字段: private static int cnt=5; 此時(shí),,并不會(huì)執(zhí)行賦值為5的操作,,而是將其初始化為0。 5)由于已經(jīng)加載到內(nèi)存了,,所以原來(lái)字節(jié)碼文件中存放的部分方法,、字段等的符號(hào)引用可以解析為其在內(nèi)存中的直接引用了,而不一定非要等到真正運(yùn)行時(shí)才進(jìn)行解析,。 6)在編譯階段,,編譯器收集所有的靜態(tài)字段的賦值語(yǔ)句及靜態(tài)代碼塊,并按語(yǔ)句出現(xiàn)的順序拼接出一個(gè)類初始化方法<clinit>(),。此時(shí),,執(zhí)行引擎會(huì)調(diào)用這個(gè)方法對(duì)靜態(tài)字段進(jìn)行代碼中編寫的初始化操作。 在Student.java中關(guān)于靜態(tài)字段的賦值及靜態(tài)代碼塊有兩處:
將按出現(xiàn)順序拼接,,形式如下: 可以通過(guò)jClassLib這個(gè)工具看到生成的<clinit>()方法的字節(jié)碼指令:
iconst_5指令把常數(shù)5入棧 putstatic #6將棧頂?shù)?賦值給Student.cnt這個(gè)靜態(tài)字段 getstatic #6 獲取Student.cnt這個(gè)靜態(tài)字段的值,,并將其放入棧頂 iconst_1 把常數(shù)1入棧 iadd 取出棧頂?shù)膬蓚€(gè)整數(shù),相加,,結(jié)果入棧 putstatic #6 取出棧頂?shù)恼麛?shù),,賦值給Student.cnt return 從當(dāng)前方法中返回,沒(méi)有任何返回值,。 從字節(jié)碼來(lái)看,,確實(shí)先后執(zhí)行了cnt =5 及 cnt++這兩行代碼。
在這里有一點(diǎn)要注意的是,,這里籠統(tǒng)的描述了下類的加載及初始化過(guò)程,,但是,實(shí)際中,,有可能只進(jìn)行了類加載,,而沒(méi)有進(jìn)行初始化工作,原因就是在程序中并沒(méi)有訪問(wèn)到該類的字段及方法等,。 此外,,實(shí)際加載過(guò)程也會(huì)相對(duì)來(lái)說(shuō)比較復(fù)雜,一個(gè)類加載之前要加載它的父類及其實(shí)現(xiàn)的接口:加載的過(guò)程可以通過(guò)java –XX:+TraceClassLoading參數(shù)查看: 如:java -XX:+TraceClassLoading Student,,信息太多,,可以重定向下:
查看輸出的loadClass.txt文件:
可以看到最先加載的是Object.class這個(gè)類,當(dāng)然了,,所有類的父類,。
直到第390行才看到自己定義的部分被加載,先是Student實(shí)現(xiàn)的接口IStudyable,,然后是其父類Person,,然后才是Student自身,,然后是一個(gè)啟動(dòng)類的加載,然后就是找到main()方法,,執(zhí)行了,。 4.執(zhí)行引擎找到main()這個(gè)入口方法,執(zhí)行其中的字節(jié)碼指令: 要了解方法的運(yùn)行,,需要先稍微了解下java棧: JVM中通過(guò)java棧,,保存方法調(diào)用運(yùn)行的相關(guān)信息,每當(dāng)調(diào)用一個(gè)方法,,會(huì)根據(jù)該方法的在字節(jié)碼中的信息為該方法創(chuàng)建棧幀,,不同的方法,其棧幀的大小有所不同,。棧幀中的內(nèi)存空間還可以分為3塊,,分別存放不同的數(shù)據(jù): 局部變量表:存放該方法調(diào)用者所傳入的參數(shù),及在該方法的方法體中創(chuàng)建的局部變量,。 操作數(shù)棧:用于存放操作數(shù)及計(jì)算的中間結(jié)果等,。 其他棧幀信息:如返回地址、當(dāng)前方法的引用等,。 只有當(dāng)前正在運(yùn)行的方法的棧幀位于棧頂,,當(dāng)前方法返回,則當(dāng)前方法對(duì)應(yīng)的棧幀出棧,,當(dāng)前方法的調(diào)用者的棧幀變?yōu)闂m?;?dāng)前方法的方法體中若是調(diào)用了其他方法,則為被調(diào)用的方法創(chuàng)建棧幀,,并將其壓入棧頂,。
注意:局部變量表及操作數(shù)棧的最大深度在編譯期間就已經(jīng)確定了,存儲(chǔ)在該方法字節(jié)碼的Code屬性中,。 簡(jiǎn)單查看Student.main()的運(yùn)行過(guò)程: 簡(jiǎn)單看下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 } 對(duì)應(yīng)的字節(jié)碼,,兩者對(duì)照著看起來(lái)更易于理解些:
注意main()方法的這幾個(gè)信息:
Mximum stack depth指定當(dāng)前方法即main()方法對(duì)應(yīng)棧幀中的操作數(shù)棧的最大深度,當(dāng)前值為5 Maximum local variables指定main()方法中局部變量表的大小,,當(dāng)前為2,,及有兩個(gè)slot用于存放方法的參數(shù)及局部變量。 Code length指定main()方法中代碼的長(zhǎng)度,。 開始模擬main()中一條條字節(jié)碼指令的運(yùn)行: 創(chuàng)建棧幀:
局部變量表長(zhǎng)度為2,,slot0存放參數(shù)args,slot1存放局部變量Student s,,操作數(shù)棧最大深度為5,。
new #7指令:在java堆中創(chuàng)建一個(gè)Student對(duì)象,并將其引用值放入棧頂。
dup指令:復(fù)制棧頂?shù)闹?,然后將?fù)制的結(jié)果入棧,。 bipush 23:將單字節(jié)常量值23入棧,。 ldc #8:將#8這個(gè)常量池中的常量即”dqrcsc”取出,,并入棧。 ldc #9:將#9這個(gè)常量池中的常量即”20150723”取出,,并入棧,。
invokespecial #10:調(diào)用#10這個(gè)常量所代表的方法,即Student.<init>()這個(gè)方法
<init>()方法,,是編譯器將調(diào)用父類的<init>()的語(yǔ)句,、構(gòu)造代碼塊、實(shí)例字段賦值語(yǔ)句,,以及自己編寫的構(gòu)造方法中的語(yǔ)句整合在一起生成的一個(gè)方法,。保證調(diào)用父類的<init>()方法在最開頭,自己編寫的構(gòu)造方法語(yǔ)句在最后,,而構(gòu)造代碼塊及實(shí)例字段賦值語(yǔ)句按出現(xiàn)的順序按序整合到<init>()方法中,。
注意到Student.<init>()方法的最大操作數(shù)棧深度為3,局部變量表大小為4,。 此時(shí)需注意:從dup到ldc #9這四條指令向棧中添加了4個(gè)數(shù)據(jù),,而Student.<init>()方法剛好也需要4個(gè)參數(shù): 雖然定義中只顯式地定義了傳入3個(gè)參數(shù),而實(shí)際上會(huì)隱含傳入一個(gè)當(dāng)前對(duì)象的引用作為第一個(gè)參數(shù),,所以四個(gè)參數(shù)依次為this,,age,name,,sid,。 上面的4條指令剛好把這四個(gè)參數(shù)的值依次入棧,進(jìn)行參數(shù)傳遞,,然后調(diào)用了Student.<init>()方法,,會(huì)創(chuàng)建該方法的棧幀,并入棧,。棧幀中的局部變量表的第0到4個(gè)slot分別保存著入棧的那四個(gè)參數(shù)值,。 創(chuàng)建Studet.<init>()方法的棧幀:
Student.<init>()方法中的字節(jié)碼指令:
aload_0:將局部變量表slot0處的引用值入棧 aload_1:將局部變量表slot1處的int值入棧 aload_2:將局部變量表slot2處的引用值入棧
invokespecial #1:調(diào)用Person.<init>()方法,同調(diào)用Student.<init>過(guò)程類似,,創(chuàng)建棧幀,,將三個(gè)參數(shù)的值存放到局部變量表等,這里就不畫圖了…… 從Person.<init>()返回之后,,用于傳參的棧頂?shù)?個(gè)值被回收了,。 aload_0:將slot0處的引用值入棧。 aload_3:將slot3處的引用值入棧,。
putfield #2:將當(dāng)前棧頂?shù)闹怠?0150723”賦值給0x2222所引用對(duì)象的sid字段,,然后棧中的兩個(gè)值出棧,。 return:返回調(diào)用方,即main()方法,,當(dāng)前方法棧幀出棧,。
重新回到main()方法中,繼續(xù)執(zhí)行下面的字節(jié)碼指令: astore_1:將當(dāng)前棧頂引用類型的值賦值給slot1處的局部變量,,然后出棧,。
aload_1:slot1處的引用類型的值入棧 iconst_5:將常數(shù)5入棧,int型常數(shù)只有0-5有對(duì)應(yīng)的iconst_x指令 bipush 6:將常數(shù)6入棧
invokevirtual #11:調(diào)用虛方法study(),,這個(gè)方法是重寫的接口中的方法,,需要?jiǎng)討B(tài)分派,所以使用了invokevirtual指令,。 創(chuàng)建study()方法的棧幀:
最大棧深度3,,局部變量表5
方法的java源碼: public int study(int a, int b){ int c = 10; int d = 20; return a+b*c-d; } 對(duì)應(yīng)的字節(jié)碼:
注意到這里,通過(guò)jClassLib工具查看的字節(jié)碼指令有點(diǎn)問(wèn)題,,與源碼有偏差…… 改用通過(guò)命令javap –v Student查看study()的字節(jié)碼指令:
bipush 10:將10入棧 istore_3:將棧頂?shù)?0賦值給slot3處的int局部變量,,即c,出棧,。 bipush 20:將20入棧 istore 4:將棧頂?shù)?0付給slot4處的int局部變量,,即d,出棧,。 上面4條指令,,完成對(duì)c和d的賦值工作。 iload_1,、iload_2,、iload_3這三條指令將slot1、slot2,、slot3這三個(gè)局部變量入棧:
imul:將棧頂?shù)膬蓚€(gè)值出棧,,相乘的結(jié)果入棧:
iadd:將當(dāng)前棧頂?shù)膬蓚€(gè)值出棧,相加的結(jié)果入棧 iload 4:將slot4處的int型的局部變量入
isub:將棧頂兩個(gè)值出棧,,相減結(jié)果入棧: ireturn:將當(dāng)前棧頂?shù)闹捣祷氐秸{(diào)用方,。
重新回到main()方法中: pop指令,將study()方法的返回值出棧 invokestatic #12 調(diào)用靜態(tài)方法getCnt()不需要傳任何參數(shù) pop:getCnt()方法有返回值,,將其出棧 aload_1:將slot1處的引用值入棧 invokevirtual #13:調(diào)用0x2222對(duì)象的run()方法,,重寫自父類的方法,需要?jiǎng)討B(tài)分派,,所以使用invokevirtual指令 return:main()返回,,程序運(yùn)行結(jié)束。
以上,就是一個(gè)簡(jiǎn)單程序運(yùn)行的大致過(guò)程,,只是今天看書的一些理解,,也許有錯(cuò)誤的地方,希望不會(huì)貽笑大方…… |
|
來(lái)自: 格瑞思n5c5alhf > 《java》