Java編譯程序和運行過程詳解

java整個編譯以及運行的過程至關繁瑣,我就舉一個簡單的例子說明:
編譯原理簡單過程:詞法分析 --> 語法分析 --> 語義分析和中間代碼生成 --> 優化 --> 目標代碼生成
Java程序從源文件建立到程序運行要通過兩大步驟:
一、Java文件會由編譯器編譯成class文件(字節碼文件),會通過編譯原理簡單過程的前三步;
二、字節碼由java虛擬機解釋運行,解釋執行即爲目標代碼生成並執行。由於java程序既要編譯的同時也要通過JVM的解釋運行,因此說Java被稱爲半解釋語言!
( "semi-interpreted" language)
 
public class Main {
 
    public static void main(String[] args) {
        Animal animal = new Animal("Tom");
        animal.printName();
    }
 
}
 
class Animal{
    private String name;
 
    public Animal(String name) {
        super();
        this.name = name;
    }
     
    public void printName(){
        System.out.println("Animal = " + this.name);
    }
}
 
第一步(編譯):建立完源文件以後,程序先要被JVM中的java編譯器進行編譯爲.class文件。java編譯一個類時,若這個類所依賴的類尚未被編譯,編譯器會自動的先編譯這個所依賴的類,而後引用;若java編譯器在指定的目錄下找不到該類所依賴的類的 .class文件或者 .java源文件,就會報"Can't found sysbol"的異常錯誤。
 
編譯後的字節碼文件格式主要分爲兩部分:常量池和方法字節碼。
  常量池記錄的是代碼出現過的字面量(文本字符串、八種基本類型的值、被聲明爲final的常量等)以及符號引用(類和方法的全限定名、字段的名稱和描述符、方法的名稱和描述符);
  方法字節碼中放的是各個方法的字節碼(依賴操做數棧和局部變量表,由JVM解釋執行)
 
第二步(運行):java類運行的過程大概分爲兩個步驟:
(1)類的加載
  加載 --> 驗證 --> 準備 --> 解析 --> 初始化(其中驗證、準備、解析統稱爲類的鏈接);(參考《深刻了解Java虛擬機》)
  加載:經過一個類的全限定名來獲取定義此類的二進制字節流(Class文件);將這個二進制字節流所表明的靜態存儲結果轉化爲方法區的運行時數據結構;在內存中生成一個java.lang.Class對象,注意:存放在方法區;
 
  驗證:驗證目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全;使用純粹的Java代碼沒法作到諸如訪問數組邊界意外的數據、將一個對象轉型爲它未實現的類型、跳轉到不存在的代碼之類的事情,若是這樣作了,編譯器將拒絕編譯;
 
  準備:準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。首先這時候進行內存分配的僅包括類變量(static修飾的變量),而不是實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在Java堆中;
 
public static int value = 123; 
 
  變量value在準備階段事後的初始值爲0而不是123,由於這時候還沒有開始執行任何Java方法,在類初始化的時候纔會將value的值賦爲123;
 
  解析:解析階段是虛擬機將class常量池內的符號引用替換爲直接引用的過程;
     符號引用:符號引用以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時能無歧義的定位到目標便可;
       直接引用:是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。有了直接引用,那引用的目標一定已經在內存中存在。
  初始化:類初始化階段是類加載過程的最後一步;在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則根據程序員經過程序制定的主觀計劃去初始化類變量和其餘資源:初始化階段是執行類構造器<clinit>( )方法的過程。
  <clinit>( )方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊(static { }塊)中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的。 
 
(2)類的執行
  須要說明的一點的是:JVM主要在程序第一次運行時主動使用類的時候,纔會當即去加載,加載完畢就會生成一個java.lang.Class對象,而且存放在方法區。換言之,JVM並非在運行時就會把全部使用到的類都加載到內存中,而是用到,不得不加載的時候,才加載進來,並且只加載一次,初始化類構造器<clinit>()方法也只執行一次,因此static{} 塊,類變量賦值語句也就只執行一次,只生成一個java.lang.Class對象!
  由Java虛擬機的執行引擎來解釋執行Java字節碼,過程:輸入字節碼文件,字節碼解析,輸出執行完的結果!(再也不贅述,請自行參考《深刻了解Java虛擬機》)
 
重點理解:根據上面的程序和概念解釋,詳解該程序運行的詳細步驟
(1)在類路徑下找到編譯好的 java 程序中獲得 Test.class 字節碼文件後,在命令行上敲 java Test,系統就會啓動一個 JVM 進程,JVM進程從classpath路徑下找到一個名爲Test.class的二進制文件,將Test.class文件中的 類信息加載到運行時數據區的方法區(JDK 8 方法區存在 堆區) 中,這一過程叫作類的加載。(只有類信息在方法區中,才能建立對象,使用類中的成員變量);
 
(2)JVM 找到main方法的主函數入口, 持有一個指向當前類(Test)常量池的指針,而常量池中的第一項發現是一個對Animal對象的符號引用,而且main方法中第一條指令是Animal animal = new Animal("Tom"),就是讓JVM建立一個Animal對象,可是方法區中尚未Animal類的類信息,因而JVM就要立刻的加載Animal類,將Animal類信息放入到方法區中,因而JVM 以一個直接指向方法區 Animal類的指針(直接引用)替換了常量池中第一項的符號引用;
 
(3)加載完Animal類的信息之後,JVM虛擬機就會在堆內存中爲一個Animal類實例分配內存,而後調用其構造函數初始化Animal實例,這個實例持有指向方法區的Animal類的類型信息(其中包含有方法表,java動態綁定的底層實現)的引用。(animal指向了Animal對象的引用會自動的放在棧中,字符串常量"Tom"會自動的放在方法區的運行時常量池中,對象會自動的放入堆區);
 
(4)當使用 animal.pringName()的時候,JVM根據棧中animal引用找到Animal對象,而後根據Animal對象持有的引用定位到方法區中Animal類的類型信息方法表,得到pringName()函數的字節碼地址,而後Java虛擬機執行引擎依賴局部變量表,操做數棧進行字節碼解釋執行,返回結果!
 
你們可能對Java執行引擎,結合局部變量表和操做數棧執行字節碼的理解不是很透徹,下來我簡單介紹一下字節碼的執行過程:
 
public int calc(){
     int a = 100;
     int b = 200;
     int c = 300;
     return (a + b) * c;     
}
 
字節碼指令展現:
  public int calc();
  Code:
  Stack=2, Locals=4, Args_size=1 //操做棧深度爲2和4個Slot局部變量表
  0:bipush 100   //將100壓入操做數棧
  2:istore_1   //將棧頂100數值存放到局變量Slot,index=1中
  3:sipush 200  //將200壓入操做數棧
  6:istore_2      //將棧頂200數值存放到局部變量Slot,index=2中
  7:sipush 300  //將300壓入操做數棧
  10:istore_3     //將棧頂200數值存放到局部變量Slot,index=3中
  11:iload_1       //將index=1的局部變量表數值壓入操做數棧(100)
  12:iload_2      //將index=2的局部變量表數值壓入操做數棧(200)
  13:iadd          //取棧頂兩個數值相加,結果壓入操做數棧(300)
  14:iload_3      //將index=3的局部變量表數值壓入操做數棧(300)
  15:imul          //取棧頂兩個數值相乘,結果壓入操做數棧(90000)
  16:ireturn      //取棧頂數值返回調用者結果
 
局部變量表 index = 0存儲當前對象自己 this 
 
參考資料:
相關文章
相關標籤/搜索