Java編譯程式和執行過程詳解
阿新 • • 發佈:2018-11-22
java整個編譯以及執行的過程相當繁瑣,我就舉一個簡單的例子說明:
編譯原理簡單過程:詞法分析 --> 語法分析 --> 語義分析和中間程式碼生成 --> 優化 --> 目的碼生成
Java程式從原始檔建立到程式執行要經過兩大步驟:
1、Java檔案會由編譯器編譯成class檔案(位元組碼檔案),會經過編譯原理簡單過程的前三步;
2、位元組碼由java虛擬機器解釋執行,解釋執行即為目的碼生成並執行。因為java程式既要編譯的同時也要經過JVM的解釋執行,所以說Java被稱為半解釋語言!
( "semi-interpreted" language)
第一步(編譯):建立完原始檔之後,程式先要被JVM中的java編譯器進行編譯為.class檔案。java編譯一個類時,若這個類所依賴的類還沒有被編譯,編譯器會自動的先編譯這個所依賴的類,然後引用;若java編譯器在指定的目錄下找不到該類所依賴的類的 .class檔案或者 .java原始檔,就會報"Can't found sysbol"的異常錯誤。
編譯後的位元組碼檔案格式主要分為兩部分:常量池和方法位元組碼。
常量池記錄的是程式碼出現過的字面量(文字字串、八種基本型別的值、被宣告為final的常量等)以及符號引用(類和方法的全限定名、欄位的名稱和描述符、方法的名稱和描述符);
https://www.cnblogs.com/blogtech/p/10000205.html
方法位元組碼中放的是各個方法的位元組碼(依賴運算元棧和區域性變量表,由JVM解釋執行) 第二步(執行):java類執行的過程大概分為兩個步驟: (1)類的載入 載入 --> 驗證 --> 準備 --> 解析 --> 初始化(其中驗證、準備、解析統稱為類的連線);(參考《深入瞭解Java虛擬機器》) 載入:通過一個類的全限定名來獲取定義此類的二進位制位元組流(Class檔案);將這個二進位制位元組流所代表的靜態儲存結果轉化為方法區的執行時資料結構;在記憶體中生成一個java.lang.Class物件,注意:存放在方法區! 驗證:驗證目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全;使用純粹的Java程式碼無法做到諸如訪問陣列邊界意外的資料、將一個物件轉型為它未實現的型別、跳轉到不存在的程式碼之類的事情,如果這樣做了,編譯器將拒絕編譯! 準備:準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都將在方法區中進行分配。首先這時候進行記憶體分配的僅包括類變數(static修飾的變數),而不是例項變數,例項變數將會在物件例項化時隨著物件一起分配在Java堆中。
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類執行的過程大概分為兩個步驟: (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 參考資料: https://wenku.baidu.com/view/32208418650e52ea55189863.html http://hxraid.iteye.com/blog/676235 http://hxraid.iteye.com/blog/428891 https://wenku.baidu.com/view/32208418650e52ea55189863.html