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

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

 

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

相關文章
相關標籤/搜索