1. 程式人生 > >Java編譯期和運行期 & JVM

Java編譯期和運行期 & JVM

實例 編譯期 his 需要 param bsp amp 新的 val

Java整個編譯以及運行的過程相當繁瑣,本文通過一個簡單的程序來簡單的說明整個流程。

首先兩張圖,描述編譯和執行的過程:

Java代碼編譯是由Java源碼編譯器來完成,流程圖如下所示:

技術分享圖片

Java字節碼的執行是由JVM執行引擎來完成,流程圖如下所示:

技術分享圖片

如下圖,Java程序從源文件創建到程序運行要經過兩大步驟:1、源文件由編譯器編譯成字節碼(ByteCode) 2、字節碼由java虛擬機解釋運行。因為java程序既要編譯同時也要經過JVM的解釋運行,所以說Java被稱為半解釋語言( "semi-interpreted" language)。

技術分享圖片
圖1 java程序編譯運行過程

下面通過以下這個java程序,來說明java程序從編譯到最後運行的整個流程。代碼如下:

Java代碼 技術分享圖片
  1. //MainApp.java
  2. public class MainApp {
  3. public static void main(String[] args) {
  4. Animal animal = new Animal("Puppy");
  5. animal.printName();
  6. }
  7. }
  8. //Animal.java
  9. public class Animal {
  10. public String name;
  11. public Animal(String name) {
  12. this.name = name;
  13. }
  14. public void printName() {
  15. System.out.println("Animal ["+name+"]");
  16. }
  17. }

第一步(編譯): 創建完源文件之後,程序會先被編譯為.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並不是在一開始就把一個程序就所有的類都加載到內存中,而是到不得不用的時候才把它加載進來,而且只加載一次。 下面是程序運行的詳細步驟:
  1. 在編譯好java程序得到MainApp.class文件後,在命令行上敲java AppMain。系統就會啟動一個jvm進程,jvm進程從classpath路徑中找到一個名為AppMain.class的二進制文件,將MainApp的類信息加載到運行時數據區的方法區內,這個過程叫做MainApp類的加載。
  2. 然後JVM找到AppMain的主函數入口,開始執行main函數。
  3. main函數的第一條命令是Animal animal = new Animal("Puppy");就是讓JVM創建一個Animal對象,但是這時候方法區中沒有Animal類的信息,所以JVM馬上加載Animal類,把Animal類的類型信息放到方法區中。
  4. 加載完Animal類之後,Java虛擬機做的第一件事情就是在堆區中為一個新的Animal實例分配內存, 然後調用構造函數初始化Animal實例,這個Animal實例持有著指向方法區的Animal類的類型信息(其中包含有方法表,java動態綁定的底層實現)的引用。
  5. 當使用animal.printName()的時候,JVM根據animal引用找到Animal對象,然後根據Animal對象持有的引用定位到方法區中Animal類的類型信息的方法表,獲得printName()函數的字節碼的地址。
  6. 開始運行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