Java編譯期和運行期 & JVM
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”的錯誤。
圖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.
Java編譯期和運行期 & JVM