1. 程式人生 > >Java程式執行過程及記憶體機制

Java程式執行過程及記憶體機制

本講將介紹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),靜態變數,方法資訊等。 ```java 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程式執行時記憶體的變化。 ```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中繼續佔用空間。 - 棧與堆的分離使得不同執行緒可以訪問同一個物件,這是一種有效的資料互動方式(共享記憶體);此外也節省了空間,因為堆中的共享常量和快取可以被所有棧訪問。 ## 參考 1. https://simplesnippets.tech/execution-process-of-java-program-in-detail-working-of-just-it-time-compiler-jit-in-detail/ 2. https://blog.csdn.net/yfqnihao/article/details/8289363 3. https://www.zhihu.com/question/29833675 有問題歡迎大家在評論區留言,轉載請註明