虛擬機器執行淺談
執行javap -verbose HelloWorld.class可以檢視class檔案的格式
以下內容轉載自java學院
初學Java,大都會用記事本寫個Hello Word,然後用CMD編譯出class檔案,最後執行一下。當控制檯輸出Hello Word的時候,一個簡單的java入門demo就完成。然而從編寫到輸出,這中間虛擬機器做了些?本文的基礎是基於jdk7以及orcal的虛擬機器。
為方便下文說明,這裡對Hello Word做了下小小的更改。見如下程式碼:
public class HelloWord {
public static int i = 100;
private static final int L = 100;
public String sayHello() {
this.add(i, L);
return “hello word!”;
}
public int add(int i, int l) {
return i + l;
}
public static void main(String[] args) {
System.out.println(new HelloWord().sayHello());
}
}
把他命名為HelloWord.java,現在開啟cmd,使用javac編譯。
得到一個HelloWord.class檔案,這個class檔案就是編譯後的位元組碼檔案,也是虛擬機器只認的檔案。為什麼說虛擬機器而不說Java虛擬機器,因為虛擬機器跟.java檔案並沒有關係,虛擬機器只跟.class有關係,而能夠編譯成class檔案的語言,不止java一種。
只要是符合虛擬機器規範定義的class格式的位元組碼檔案都是可以被虛擬機器處理執行的,如果想要看class格式,jdk提供了一個工具javap,使用cmd定位到剛剛生成class檔案目錄,執行:
javap -verbose HelloWord.class
將會得到以下結果:
SourceFile: “HelloWord.java”
以下挑一些比較重要的屬性進行介紹:
minor version:編譯副版本號
najor version:編譯主版本號,51代表是jdk7
Constant pool:常量池,這個裡面存放的是類的一些基本資訊、類變數以及引用符號等
再往下就是具體方法的編譯,取一個sayHello方法為例:
descriptor:引用符號
flags:方法訪問許可權標誌
code:方法體編譯後的位元組碼指令
stack:預計佔用的棧幀深度(後面會介紹)
locals:預計佔用的空間內容
args_size:引數個數,例項方法預設會把this作為第一個引數,靜態方法不會
LineNumberTable:對應到原始檔中的行號
LocalVariableTable:區域性變量表
不用理會那些位元組碼做什麼的,只要知道那就是你寫的程式碼編譯後的樣子就好了,其實有所瞭解的應該知道那屬於棧集指令。
現在,我們已經做到了java到class的過程,並且看到class是如果編譯的。下一步就是執行他,需要用到類載入器。
類載入器遵循雙親委派模型,當類載入器載入一個類的時候不會立即去載入,而是先讓父級類載入器載入,這樣逐級載入,只有當父級無法載入的時候才會自行載入。一般在java中有啟動類載入器、擴充套件類載入器、應用程式類載入器以及自定義載入器。
類載入器載入HelloWord.class到虛擬機器,就開始了class檔案在虛擬機器的流程,下圖是一個class檔案在虛擬機器的生命週期:
載入
1、獲取二進位制流
2、轉化成方法區執行時資料結構
3、在堆中生成物件
驗證
1、檔案格式驗證:二進位制流是否符合class規範,並且能被當前虛擬機器能夠的版本之內 (類)
2、元資料驗證:分析位元組碼,保證其描述的資訊符合java語言規範(類)
3、位元組碼驗證:進行資料流和控制流分析(方法體)
4、符號引用驗證:驗證該類引用的其他類,欄位,方法是否符合規範(類外引用的類, 欄位,方法)
準備
HelloWord執行記憶體機制-淺談虛擬機器執行流程(面試38講)
方法區內為類變數,即被static修飾的變數,分配記憶體並設定類變數初始值,並非程式碼中設定的初始值,而是最原始的值,final修飾的常量除外.
HelloWord執行記憶體機制-淺談虛擬機器執行流程(面試38講)
解析
1、類或介面的解析
2、欄位解析
3、類方法解析
4、介面方法解析
初始化
1、收集類中類變數的賦值動作、靜態語句塊合併產生
2、先執行父類的
3、若類中沒有類變數和靜態語句快則不生成
4、是同步的,同一時刻只會有一個執行緒能夠執行,其他要使用這個的執行緒等待
瞭解了以上知識,現在我們來看System.out.println(new HelloWord().sayHello());這句話,首先建立一個物件,然後呼叫方法,方法首先又呼叫了一個方法,最後返回一個字串,輸出。
在虛擬機器是如何進行這一步操作呢?虛擬機器資料模型分為pc暫存器、棧、本地方法棧、堆、方法區、常量池等。類被解析到方法區,物件被初始化到堆,常量放到常量池,而執行則在棧中進行,這裡一個不得不引入一個概念-棧幀。
棧幀用於支援虛擬機器進行方法呼叫和方法執行的資料結構,是虛擬機器執行時資料區中的虛擬機器棧的棧元素。村輸了方法的區域性變量表、運算元棧、動態連線和方法返回地址等資訊。每一個方法從呼叫開始到執行完成額過程,就對應著一個棧幀的虛擬機器棧裡面從入棧到出棧的過程,每個棧幀佔用的內容空間和棧深度都在在編譯期定義好的(code屬性下的locals和stack)。
HelloWord執行記憶體機制-淺談虛擬機器執行流程(面試38講)
區域性變量表
是一組變數值儲存空間,用於存放方法引數和方法內部定義的區域性變數。
運算元棧(後入先出)
方法執行的時候,剛開始是空的,經過指令向其執行入棧和出棧
動態連線
每個棧幀都包含一個指向執行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支援方法呼叫過程中的動態連線。
方法返回地址
方法執行後有兩種方式退出這個方法。第一個是執行引擎遇到任意一個方法返回的位元組碼指令,另一個是執行中遇到異常。無論哪種退出,都需要返回到方法被呼叫的位置,程式才能繼續執行。
看到這就不難理解為什麼sayHello的stack為什麼是3了,這裡雖然不涉及但是應該補充的知識點如下:
1、java是採用棧指令集,不依賴於暫存器,相比於暫存器指令集,其程式碼量多,執行速度相對慢,但可移植性大不受硬體限制。
2、slot:區域性變數空間
3、java記憶體模型的主要目標是定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中取出變數這樣的底層細節。這裡的變數與java變成中的變數略有區別,他包括了例項欄位、靜態欄位和構成陣列物件的元素,但不包括區域性變數和方法引數,因為後者是執行緒私有的,不會被共享。
HelloWord執行記憶體機制-淺談虛擬機器執行流程(面試38講)
4、java記憶體原型的特性:原子性、可見性、有序性。
原子性:read、load、assign、user、store、write操作原子性
可見性:volatile保證資料使用前從主記憶體獲取資料和更改後立即重新整理回主記憶體、synchronized對應lock和unlock,對一個變數執行unlock之前,必須先把此變數同步回主記憶體中、final修飾的資料,一旦被初始化完成,那麼其他執行緒就能看見final的值。
有序性:如果在本縣城內觀察,所有的操作都是有序的,如果在一個執行緒中觀察另一個執行緒,所有的操作都是無序的。
5、先行發生原則:
程式次序規則:同一執行緒內按照程式程式碼流程順序先行發生(執行)
管城鎖定規則:同一個鎖的unlock先行發生lock之前(時間上)
volatile變數規則:valotile變數寫操作先行發生讀操作(時間上)
執行緒啟動規則:Thread物件的start方法先行發生於此執行緒的每一個動作
執行緒終止規則:執行緒中的所有操作都先行發生於此執行緒的終止檢測。
執行緒中斷規則:對執行緒interrupt方法的呼叫先行發生於被終端縣城的程式碼檢測到中斷事件的發生。
物件終結規則:一個物件的初始化完成(建構函式執行結束)先行發生於他的finalize方法的開始
傳遞性:如果操作a先行發生於操作b,操作b先行發生於操作c,那就可以得出操作a先行發生於操作c的結論
volatile使用原則:對修飾的變數的修改不依賴變數的原值。