Java整個編譯以及啟動並執行過程相當繁瑣,本文通過一個簡單的程式來簡單的說明整個流程。
首先兩張圖,描述編譯和執行的過程:
Java代碼編譯是由Java源碼編譯器來完成,流程圖如下所示:
Java位元組碼的執行是由JVM執行引擎來完成,流程圖如下所示:
如下圖,Java程式從源檔案建立到程式運行要經過兩大步驟:1、源檔案由編譯器編譯成位元組碼(ByteCode) 2、位元組碼由java虛擬機器解釋運行。因為java程式既要編譯同時也要經過JVM的解釋運行,所以說Java被稱為半解釋語言( "semi-interpreted" language)。
圖1 java程式編譯運行過程
下面通過以下這個java程式,來說明java程式從編譯到最後啟動並執行整個流程。代碼如下:
Java代碼 //MainApp.java public class MainApp { public static void main(String[] args) { Animal animal = new Animal("Puppy"); animal.printName(); } } //Animal.java public class Animal { public String name; public Animal(String name) { this.name = name; } public void printName() { System.out.println("Animal ["+name+"]"); } }
第一步(編譯): 建立完源檔案之後,程式會先被編譯為.class檔案。Java編譯一個類時,如果這個類所依賴的類還沒有被編譯,編譯器就會先編譯這個被依賴的類,然後引用,否則直接引用,這個有點象make。如果java編譯器在指定目錄下找不到該類所其依賴的類的.class檔案或者.java源檔案的話,編譯器話報“cant find symbol”的錯誤。 編譯後的位元組碼檔案格式主要分為兩部分:常量池和方法位元組碼。常量池記錄的是代碼出現過的所有token(類名,成員變數名等等)以及符號引用(方法引用,成員變數引用等等);方法位元組碼放的是類中各個方法的位元組碼。下面是MainApp.class通過反組譯碼的結果,我們可以清楚看到.class檔案的結構: 圖2 MainApp類常量池
圖3 MainApp類方法位元組碼
最後產生的class檔案由以下部分組成: 結構資訊。包括class檔案格式版本號碼及各部分的數量與大小的資訊 中繼資料。對應於Java源碼中聲明與常量的資訊。包含類/繼承的超類/實現的介面的聲明資訊、域與方法聲明資訊和常量池 方法資訊。對應Java源碼中語句和運算式對應的資訊。包含位元組碼、異常處理器表、求值棧與局部變數區大小、求值棧的類型記錄、偵錯符號資訊
第二步(運行):java類啟動並執行過程大概可分為兩個過程:1、類的載入 2、類的執行。需要說明的是:JVM主要在程式第一次主動使用類的時候,才會去載入該類。也就是說,JVM並不是在一開始就把一個程式就所有的類都載入到記憶體中,而是到不得不用的時候才把它載入進來,而且只載入一次。 下面是程式啟動並執行詳細步驟: 在編譯好java程式得到MainApp.class檔案後,在命令列上敲java AppMain。系統就會啟動一個jvm進程,jvm進程從classpath路徑中找到一個名為AppMain.class的二進位檔案,將MainApp的類資訊載入到運行時資料區的方法區內,這個過程叫做MainApp類的載入。 然後JVM找到AppMain的主函數入口,開始執行main函數。 main函數的第一條命令是Animal animal = new Animal("Puppy");就是讓JVM建立一個Animal對象,但是這時候方法區中沒有Animal類的資訊,所以JVM馬上載入Animal類,把Animal類的類型資訊放到方法區中。 載入完Animal類之後,Java虛擬機器做的第一件事情就是在堆區中為一個新的Animal執行個體分配記憶體, 然後調用建構函式初始化Animal執行個體,這個Animal執行個體持有著指向方法區的Animal類的類型資訊(其中包含有方法表,java動態綁定的底層實現)的引用。 當使用animal.printName()的時候,JVM根據animal引用找到Animal對象,然後根據Animal對象持有的引用定位到方法區中Animal類的類型資訊的方法表,獲得printName()函數的位元組碼的地址。 開始運行printName()函數。
圖4 java程式運行過程 特別說明:java類中所有public和protected的執行個體方法都採用動態綁定機制,所有私人方法、靜態方法、構造器及初始化方法<clinit>都是採用靜態繫結機制。而使用動態綁定機制的時候會用到方法表,靜態繫結時並不會用到。
Ps:
方法重載:這個是發生在編譯時間的。方法重載也被稱為編譯時間多態,因為編譯器可以根據參數的類型來選擇使用哪個方法。
1 2 3 4 |
public class { public static void evaluate(String param1); // method #1 public static void evaluate( int param1); // method #2 } |
如果編譯器要編譯下面的語句的話:
1 |
evaluate(“My Test Argument passed to param1”); |
它會根據傳入的參數是字串常量,產生調用#1方法的位元組碼
方法覆蓋:這個是在運行時發生的。方法重載被稱為運行時多態,因為在編譯期編譯器不知道並且沒法知道該去調用哪個方法。JVM會在代碼啟動並執行時候做出決定。
1 2 3 4 5 6 7 8 9 10 11 12 |
public class A { public int compute( int input) { //method #3 return 3 * input; } } public class B extends A { @Override public int compute( int input) { //method #4 return 4 * input; } } |
子類B中的compute(..)方法重寫了父類的compute(..)方法。如果編譯器遇到下面的代碼:
1 2 3 |
public int evaluate(A reference, int arg2) { int result = reference.compute(arg2); } |
編譯器是沒法知道傳入的參數reference的類型是A還是B。因此,只能夠在運行時,根據賦給輸入變數“reference”的對象的類型(例如,A或者B的執行個體)來決定調用方法#3還是方法#4.