本講將介紹Java代碼是如何一步步運行起來的,其中涉及的編譯器,類加載器,位元組碼校驗器,解釋器和JIT編譯器在整個程序中是發揮著怎樣的作用,此外還會介紹Java程式所占用的記憶體是被如何管理的:堆、堆疊和方法區都各自負責存盤哪些內容,最后用一小塊代碼示例來幫助理解Java程式運行時記憶體的變化,
Java程式執行程序
- 步驟 1: 寫源代碼,源代碼將以.java的檔案格式保存在電腦硬碟中,
- 步驟 2: 編譯器(compiler)檢查是否存在編譯期錯誤(例如缺少分號,關鍵字拼寫錯誤等),若通過檢測,編譯器就會將源代碼翻譯成位元組碼(bytecode),以.class的檔案格式保存在電腦硬碟中,
- 步驟 3: 若要運行此Java程式,JVM中會有一個叫類加載器(class loader)的內置程式把位元組碼從硬碟載入到正位于記憶體中的JVM里去,
- 步驟 4: JVM中還有一個叫位元組碼校驗器(bytecode verifier)的內置程式檢測是否存在運行期錯誤(例如堆疊溢位),若通過檢測,位元組碼校驗器就會將位元組碼傳遞給解釋器(interpreter),
- 步驟 5: 解釋器會對位元組碼進行逐行翻譯,將其翻譯成當前所在系統可以理解的機器碼(machine code),
- 步驟 6:將機器碼交給作業系統,作業系統會以main方法作為入口開始執行程式,至此,一個Java程式就這樣運行起來了,
細心的讀者可能注意到了,在流程圖中還涉及到一個叫JIT的東西在步驟中沒有被解釋,那么JIT編譯器(Just-In-Time Compiler)是如果參與行程式的執行程序中呢?讓我們來看以下兩個例子,
- 情況 1: 解釋器對代碼進行逐行解釋,正如我們在步驟中所介紹的,
- 情況 2: 這是JIT編譯器參與進Java執行程序的情況,JIT編譯器會掃描所有代碼并對其進行優化,例如此時它發現最后一行代碼是重復多余的,就會將其移除,只傳遞前4行代碼給解釋器,這樣解釋器就能運行地更快速高效,畢竟少了一行多余的代碼需要翻譯,
當然,這只是JIT編譯器的優化手段之一,不同公司設計的JIT編譯器對Java程式的運行會有不同的優化方式,此外需要知道的是,JIT編譯器并不是每次都會參與到執行程序中來,
記憶體機制
在步驟3中我們談到位元組碼會被類加載器載入到記憶體,那么載入之后JVM是如何對其進行記憶體管理的呢?
通常,在載入記憶體后,一個Java程式所占用的記憶體會被大致分為3塊區域:堆(heap),堆疊(stack)和方法區(method area),
堆:存放new出來的東西,
堆疊:存放區域變數,
方法區:型別資訊,欄位資訊,常量池(constant pool),靜態變數,方法資訊等,
public final class Student extends Object implements Serializable { // 1.類資訊
// 2.物件欄位資訊
private String name;
private int score;
// 3.常量池
public final int id = 0;
public final String gender = "male";
// 4.靜態變數
public static int a = 0;
// 5.方法資訊
public int getid() {
return id;
}
}
PC暫存器:存放將要執行的指令的地址,(因為機器的腦子不靈活,所以需要一塊專門的區域幫他記住執行到哪一步,不然它會忘記)
本地方法堆疊:與JVM堆疊所發揮的作用是非常相似的,其區別不過是JVM堆疊為Java方法服務,而本地方法堆疊則是為使用到的Native方法服務,有的虛擬機(例如Sun HotSpot虛擬機)甚至直接就把本地方法堆疊和虛擬機堆疊合二為一,
每個執行緒擁有各自獨立的(虛擬機)堆疊、PC暫存器和本地方法堆疊,而堆和方法區則是所有執行緒共享的,
最后讓我們通過一個小例子來理解Java程式執行時記憶體的變化,
public class Person {
int id;
int age;
Person(int id1, int age1) {
id = id1;
age = age1;
}
public static void main(String[] args) {
Person Tom = new Person(1, 25);
}
}
首先,在stack中申請了一塊記憶體,這塊記憶體區域名字叫Tom,此時區域里存盤的內容為null,
接著,呼叫Person的構造方法,方法的引數屬于區域變數,因此在stack中有兩塊區域分別存放id1和age1,
通過構造方法,可以new出來一個Person的物件,這個物件連帶著其成員變數會被存放在heap中,成員變數id和age的值由存放在stack中的區域變數id1和age1賦予,
最后,將這個物件的參考值(類似于地址)傳遞給Tom,通過參考值我們就可以找到這個物件,
(注意:位于stack中的id1和age1會隨著構造方法呼叫的結束而消失,這里為了更好地表現全程序,因此保留在圖中,)
關于堆疊和堆的其他小知識
- 堆疊和堆的大小都是固定為一個默認值的,它們作為jvm的引數設定好了,不同的jvm設定的引數不同,相應的堆疊和堆的大小也就不同,
- 堆疊是運行時的單位:里面存盤的資訊都是跟當前執行緒相關的,包括區域變數、程式運行狀態、方法回傳值等;而堆是存盤的單位:它只負責存盤物件,
- 當一個方法呼叫結束后,方法里的區域變數會隨之消失,不會在stack中繼續占用空間,
- 堆疊與堆的分離使得不同執行緒可以訪問同一個物件,這是一種有效的資料互動方式(共享記憶體);此外也節省了空間,因為堆中的共享常量和快取可以被所有堆疊訪問,
參考
- https://simplesnippets.tech/execution-process-of-java-program-in-detail-working-of-just-it-time-compiler-jit-in-detail/
- https://blog.csdn.net/yfqnihao/article/details/8289363
- https://www.zhihu.com/question/29833675
有問題歡迎大家在評論區留言,轉載請注明出處,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/233378.html
標籤:Java
