java拾遺4----一個簡單java程序的運行全過程


簡單說來,一個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()返回,程序運行結束。

 

以上,就是一個簡單程序運行的大致過程,只是今天看書的一些理解,也許有錯誤的地方,希望不會貽笑大方……


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM