歡迎光臨
每天分享高質量文章

一個簡單java程式的運行全過程

點擊上方“Java技術驛站”,選擇“置頂公眾號”。

有內涵、有價值的文章第一時間送達!

精品專欄

 

作者:某人的喵星人
原文:https://www.cnblogs.com/dqrcsc/p/4671879.html

簡單說來,一個java程式的運行需要編輯原始碼、編譯生成class檔案、加載class檔案、解釋或編譯運行class中的位元組碼指令。

下麵有一段簡單的java原始碼,通過它來看一下java程式的運行流程:

  1. class Person{

  2.       private String name;

  3.       private int age;

  4.       public Person(int age, String name){

  5.              this.age = age;

  6.              this.name = name;

  7.       }

  8.       public void run(){

  9.       }

  10. }

  11. interface IStudyable{

  12.       public int study(int a, int b);

  13. }

  14. public class Student extends Person implements IStudyable{

  15.       private static int cnt=5;

  16.       static{

  17.            cnt++;

  18.       }

  19.       private String sid;

  20.       public Student(int age, String name, String sid){

  21.              super(age,name);

  22.              this.sid = sid;

  23.       }

  24.       public void run(){

  25.              System.out.println("run()...");

  26.       }

  27.       public int study(int a, int b){

  28.              int c = 10;

  29.              int d = 20;

  30.              return a+b*c-d;

  31.       }

  32.       public static int getCnt(){

  33.              return cnt;

  34.       }

  35.       public static void main(String[] args){

  36.              Student s = new Student(23,"dqrcsc","20150723");

  37.              s.study(5,6);

  38.              Student.getCnt();

  39.              s.run();

  40.       }

  41. }

1、編輯原始碼

無論是使用記事本還是別的什麼,編寫上面的代碼,然後儲存到Student.java,我直接就放到桌面了

2.編譯生成class位元組碼檔案

打開命令視窗,輸入命令javac Student.java將該原始碼檔案編譯生成.class位元組碼檔案。

由於在原始碼檔案中定義了兩個類,一個接口,所以生成了3個.clsss檔案:

這樣能在java虛擬機上運行的位元組碼檔案就生成了

啟動java虛擬機運行位元組碼檔案

在命令列中輸入 javaStudent 這個命令,就啟動了一個 java 虛擬機,然後加載 Student.class 位元組碼檔案到記憶體,然後運行記憶體中的位元組碼指令了。

我們從編譯到運行 java 程式,只輸入了兩個命令,甚至,如果使用集成開發環境,如 eclipse,只要 ctrl+s 儲存就完成了增量編譯,只需要按下一個按鈕就運行了 java 程式。但是,在這些簡單操作的背後還有一些操作……

從原始碼到位元組碼

位元組碼檔案,看似很微不足道的東西,卻真正實現了 java 語言的跨平臺。各種不同平臺的虛擬機都統一使用這種相同的程式儲存格式。更進一步說,jvm 運行的是 class 位元組碼檔案,只要是這種格式的檔案就行,所以,實際上 jvm 並不像我之前想象地那樣與 java 語言緊緊地捆綁在一起。如果非常熟悉位元組碼的格式要求,可以使用二進制編輯器自己寫一個符合要求的位元組碼檔案,然後交給 jvm 去運行;或者把其他語言編寫的原始碼編譯成位元組碼檔案,交給 jvm 去運行,只要是合法的位元組碼檔案, jvm 都會正確地跑起來。所以,它還實現了跨語言……

通過 jClassLib 可以直接查看一個 .class 檔案中的內容,也可以給 JDK 中的 javap 命令指定引數,來查看 .class 檔案的相關信息:

javapvStudent

好多輸出,在命令列視窗查看不是太方便,可以輸出重定向下:

javapvStudent>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的功能模塊主要包括類加載器、執行引擎和垃圾回收系統

類加載器加載 Student.class 到記憶體

  1. 類加載器會在指定的 classpath 中找到 Student.class 這個檔案,然後讀取位元組流中的資料,將其儲存在方法區中。

  2. 會根據 Student.class 的信息建立一個 Class 物件,這個物件比較特殊,一般也存放在方法區中,用於作為運行時訪問 Student 類的各種資料的接口。

  3. 必要的驗證工作,格式、語意等

  4. 為 Student 中的靜態欄位分配記憶體空間,也是在方法區中,併進行零初始化,即數字型別初始化為 0 ,boolean 初始化為 false,取用型別初始化為 null 等。在 Student.java 中只有一個靜態欄位: privatestaticintcnt=5; 此時,並不會執行賦值為5的操作,而是將其初始化為0。

  5. 由於已經加載到記憶體了,所以原來位元組碼檔案中存放的部分方法、欄位等的符號取用可以解析為其在記憶體中的直接取用了,而不一定非要等到真正運行時才進行解析。

  6. 在編譯階段,編譯器收集所有的靜態欄位的賦值陳述句及靜態代碼塊,並按陳述句出現的順序拼接出一個類初始化方法 ()。此時,執行引擎會呼叫這個方法對靜態欄位進行代碼中編寫的初始化操作。

在 Student.java 中關於靜態欄位的賦值及靜態代碼塊有兩處:

  1. private static int cnt=5;

  2. static{

  3.    cnt++;

  4. }

將按出現順序拼接,形式如下:

  1. void (){

  2.    cnt = 5;

  3.    cnt++;

  4. }

可以通過 jClassLib 這個工具看到生成的 () 方法的位元組碼指令:

  • iconst_5 :指令把常數5入棧

  • putstatic #6:將棧頂的5賦值給 Student.cnt 這個靜態欄位

  • getstatic #6:獲取Student.cnt這個靜態欄位的值,並將其放入棧頂

  • iconst_1:把常數1入棧

  • iadd:取出棧頂的兩個整數,相加,結果入棧

  • putstatic #6:取出棧頂的整數,賦值給Student.cnt

  • return:從當前方法中傳回,沒有任何傳回值。

從位元組碼來看,確實先後執行了 cnt=5cnt++ 這兩行代碼。

在這裡有一點要註意的是,這裡籠統的描述了下類的加載及初始化過程,但是,實際中,有可能只進行了類加載,而沒有進行初始化工作,原因就是在程式中並沒有訪問到該類的欄位及方法等。

此外,實際加載過程也會相對來說比較複雜,一個類加載之前要加載它的父類及其實現的接口:加載的過程可以通過java –XX:+TraceClassLoading引數查看:

如: java-XX:+TraceClassLoadingStudent,信息太多,可以重定向下:

查看輸出的 loadClass.txt 檔案:

可以看到最先加載的是 Object.class 這個類,當然了,所有類的父類。

直到第 390 行才看到自己定義的部分被加載,先是 Studen t實現的接口 IStudyable ,然後是其父類 Person ,然後才是 Student 自身,然後是一個啟動類的加載,然後就是找到 main() 方法,執行了。

執行引擎找到 main()

要瞭解方法的運行,需要先稍微瞭解下 java 棧:

JVM 中通過 java 棧,儲存方法呼叫運行的相關信息,每當呼叫一個方法,會根據該方法的在位元組碼中的信息為該方法創建棧幀,不同的方法,其棧幀的大小有所不同。棧幀中的記憶體空間還可以分為3塊,分別存放不同的資料:

  • 區域性變數表:存放該方法呼叫者所傳入的引數,及在該方法的方法體中創建的區域性變數。

  • 運算元棧:用於存放運算元及計算的中間結果等。

  • 其他棧幀信息:如傳回地址、當前方法的取用等。

只有當前正在運行的方法的棧幀位於棧頂,當前方法傳回,則當前方法對應的棧幀出棧,當前方法的呼叫者的棧幀變為棧頂;當前方法的方法體中若是呼叫了其他方法,則為被呼叫的方法創建棧幀,並將其壓入棧頂。

註意:區域性變數表及運算元棧的最大深度在編譯期間就已經確定了,儲存在該方法位元組碼的Code屬性中。

簡單查看 Student.main() 的運行過程

簡單看下main()方法:

  1. public static void main(String[] args){

  2.    Student s = new Student(23,"dqrcsc","20150723");

  3.    s.study(5,6);

  4.    Student.getCnt();

  5.    s.run();

  6. }

對應的位元組碼,兩者對照著看起來更易於理解些:

註意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.()這個方法

() 方法,是編譯器將呼叫父類的 () 的陳述句、構造代碼塊、實體欄位賦值陳述句,以及自己編寫的構造方法中的陳述句整合在一起生成的一個方法。保證呼叫父類的 () 方法在最開頭,自己編寫的構造方法陳述句在最後,而構造代碼塊及實體欄位賦值陳述句按出現的順序按序整合到 () 方法中。

註意到 Student.<init>() 方法的最大運算元棧深度為 3,區域性變數表大小為 4。

此時需註意:從 dup 到 ldc #9 這四條指令向棧中添加了4個資料,而Student.()方法剛好也需要4個引數:

  1. public Student(int age, String name, String sid){

  2.    super(age,name);

  3.    this.sid = sid;

  4. }

雖然定義中只顯式地定義了傳入3個引數,而實際上會隱含傳入一個當前物件的取用作為第一個引數,所以四個引數依次為this,age,name,sid。

上面的4條指令剛好把這四個引數的值依次入棧,進行引數傳遞,然後呼叫了Student.()方法,會創建該方法的棧幀,併入棧。棧幀中的區域性變數表的第0到4個slot分別儲存著入棧的那四個引數值。

創建 Studet.<init>() 方法的棧幀:

Student.()方法中的位元組碼指令:

  • aload_0:將區域性變數表slot0處的取用值入棧

  • aload_1:將區域性變數表slot1處的int值入棧

  • aload_2:將區域性變數表slot2處的取用值入棧

  • invokespecial #1:呼叫Person.()方法,同呼叫Student.過程類似,創建棧幀,將三個引數的值存放到區域性變數表等,這裡就不畫圖了……

從Person.()傳回之後,用於傳參的棧頂的3個值被回收了。

  • aload_0:將slot0處的取用值入棧。

  • aload_3:將slot3處的取用值入棧。

  • putfield #2:將當前棧頂的值”20150723”賦值給0x2222所取用物件的sid欄位,然後棧中的兩個值出棧。

  • return:傳回呼叫方,即main()方法,當前方法棧幀出棧。

重新回到main()方法中,繼續執行下麵的位元組碼指令:

astore_1:將當前棧頂取用型別的值賦值給slot1處的區域性變數,然後出棧。

  • aload_1:slot1處的取用型別的值入棧

  • iconst5:將常數5入棧,int型常數只有0-5有對應的iconstx指令

  • bipush 6:將常數6入棧

  • invokevirtual #11:呼叫虛方法study(),這個方法是重寫的接口中的方法,需要動態分派,所以使用了invokevirtual指令。

創建study()方法的棧幀:

最大棧深度3,區域性變數表5

方法的java原始碼:

  1. public int study(int a, int b){

  2.     int c = 10;

  3.     int d = 20;

  4.     return a+b*c-d;

  5. }

對應的位元組碼:

註意到這裡,通過 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的賦值工作。

iload1、iload2、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()傳回,程式運行結束。

以上,就是一個簡單程式運行的大致過程

赞(0)

分享創造快樂

© 2021 知識星球   网站地图