Java檔案的執行過程大致為:Java檔案通過編譯器(javac)編譯為.class檔案,然後類載入子系統將class檔案載入進執行時資料區,再通過執行引擎將位元組碼檔案編譯/解析為機器指令。
Java虛擬機器器在執行Java程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區域,這個資料區域就叫執行時資料區。執行時資料區主要包含了PC暫存器(程式計數器)、Java虛擬機器器棧、本地方法棧、Java堆、方法區以及執行時常數池,這其中Java堆、方法區跟Java虛擬機器器棧是學習的重點。
(簡圖)
(詳細圖)
程式計數器(Program counter Register)是記錄當前執行緒正在執行的位元組碼的地址。程式計數器是執行緒隔離的,每一個執行緒在工作的時候都有一個獨立的計數器。因為Java是可以多執行緒執行的,一個執行緒執行到一半可能因為CPU時間片輪轉切換到了另外一個執行緒,在切換回之前執行緒的時候,需要回到執行緒上次的執行位置,所以要執行緒私有。
程式計數器的特點
程式計數器具有執行緒隔離性
程式計數器佔用的記憶體空間非常小,可以忽略不計
程式計數器是java虛擬機器器規範中唯一一個沒有規定任何OutofMemeryError的區域
程式執行的時候,程式計數器是有值的,其記錄的是程式正在執行的位元組碼的地址
執行native本地方法時,程式計數器的值為空。原因是native方法是java通過jni呼叫本地C/C++庫來實現,非java位元組碼實現,所以無法統計
Java虛擬機器器棧(Java Virtual Machine Stacks)也是執行緒私有的,它的生命週期與執行緒相同。虛擬機器器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同到都會建立一個棧幀(Stack Frame)用於儲存區域性量表、運算元棧、動態連結、方法出口等資訊。棧幀是Java方法執行時的基礎資料結構,每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬棧中從入棧到出棧的過程(說人話就是要執行一個方法,將該方法的棧幀壓入棧頂,方法執行完成其棧幀出棧)。在JVM裡面,棧幀的操作只有兩種:出棧和入棧。正在被執行緒執行的方法稱為當前執行緒方法,而該方法的棧幀就稱為當前幀。
區域性變數表存放了編譯期可知的各種基本資料型別(boolean、byte、char、short、int、long、float、double)、物件參照(reference型別,它不等同於物件本身,可能是一個指向物件始地址的參照指標,也可能是指向一個代表物件的控制程式碼或其地與此物件相關的位置)和returnAddress型別(指向了一條位元組碼指令的地址)。
在Java虛擬機器器規範中,對這個區域定了兩種異狀況:如果執行緒請求的棧深度大於虛擬機器器所允許的深度,將丟擲StackOverFlowError異常;一般的虛擬機器器棧都是可延伸的,如果擴充套件時無法豐請到足夠的記憶體,就會丟擲OutOfMemoryError異常,可以通過-Xss設定每個執行緒的堆疊大小。
Java虛擬機器器棧的結構如下圖所示:Java虛擬機器器棧的生命週期與執行緒一致,一個方法對應一塊棧幀記憶體區域,棧幀中包含區域性變數表、運算元棧、動態連結、方法出口等資訊。拿下面程式碼舉例,程式執行main(),main()先壓入棧頂,然後main()方法中new了一個Math物件,math變數是指向堆中Math物件的參照,math變數就屬於區域性變數表,建立Math物件之後,呼叫了其compute(),然後compute()壓入棧頂,compute方法執行完成後其棧幀出棧,然後根據程式計數器記錄程式執行的行號,繼續回到main方法執行,main方法中已經沒有其他執行指令了,則main方法退出,main方法對應的棧幀出棧,虛擬機器器棧中已經沒有其他棧幀,main執行緒生命週期結束。
本地方法棧(Native Method Stack)與虛擬機器器棧非常相似,也是執行緒私有的,它們的區別不過是虛擬機器器棧執行的是Java方法(也就是位元組碼),而本地方法棧用到的是Native方法。與虛擬機器器戰一樣。本地方法棧區域也會出現StackOverFlowError和OutOfMemoryError異常。
方法區(Method Area),是各個執行緒共用的記憶體區域,它用於儲存虛擬機器器載入的:類資訊+普通常數+靜態常數+編譯器編譯後的程式碼等等。雖然JVM規範將方法區描述為堆的一個邏輯部分,但它卻還有一個別名叫做Non一Heap(非堆),目的就是要和堆分開。這部分儲存的是執行時必須的類相關資訊,裝載進此區域的資料是不會被垃圾收集器回收的,只有關閉Jvm才會釋放這塊區域佔用的記憶體。
對於Hotspot虛擬機器器,很多開發者習慣將方法區稱之為「永久代(Parmanent Gen)",但嚴格本質上說兩者不同,或者說使用永久代來實現方法區而己,永久代是方法區(相當於是一個介面interface)的一個實現,idkl.7的版本中,己經將原本放在永久代的字串常數池移走。Jdk1.7中方法區是用永久代實現的,到1.8中是用元空間(MetaSpace)實現的,而元空間使用的是直接記憶體。
根據Java虛擬機器器規範的規定,當方法區無法滿足記憶體分配需求時會丟擲OutOfMemoryError異常。可以通過-XX:PermSize和 -XX:MaxPermSize來分別設定永久區最小、最大空間。
執行時常數池(Runtime Constant Pool)是方法區的一部分。Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常數池,用於存放編譯期生產的各種字面量和符號參照,這部分內容在類載入後進入方法區的執行時常數池中存放。
常數池中主要存放兩大類常數:字面量(Literal)和符號參照(Symbolic References)。字面量比較接近於Java語言層面的常數概念,如文字字串、宣告為final的常數值等。而符號參照則屬於編譯原理方面的概念,包括了下面三類常數:
類和介面的全限定名(Fully Qualified Name)
欄位的名稱和描述符(Descriptor)
方法的名稱和描述符
Java程式碼在進行Javac編譯的時候,並不像C和C++那樣有「連線」這一步驟,而是在虛擬機器器載入Class檔案的時候進行動態連線。也就是說,在Class檔案中不會儲存各個方法、欄位的最終記憶體佈局資訊,因此這些欄位、方法的符號參照不經過執行期轉換的話無法得到真正的記憶體人口地址,也就無法直接被虛擬機器器使用。當虛擬機器器執行時,需要從常數池獲得對應的符號參照,再在類建立時或執行時解析、翻譯到具體的記憶體地址之中。
Java語言不要求常數一定只有編譯器才能產生,執行時也可能將新的常數放入池中,該特性用的比較多的就是String類的intern()方法。執行時常數池是方法區的一部分,在記憶體不夠時,也會丟擲OutOfMemoryError異常。
對於大多數應用來說,Java堆(Java Heap)是Java虛擬機器器所管理的記憶體中最大的一塊。Java堆是執行緒共用的,在虛擬機器器啟動時建立。此記憶體區域的唯一目的就是存放物件範例,幾乎所有的物件範例都在這裡分配記憶體。這一點在Java擬機規範中的描述是:所有的物件範例以及陣列都要在堆上分配,但是著JIT編譯器的發展與逸分析技術逐漸成熟,棧上分配、標量替換優化技術會導致一些微妙的變化發生,所有的物件都分配在堆上也漸漸變得不是那麼"絕對"了。
Java堆是被收集管理的主要區域,因此很多時候也被稱做"GC堆"(Garbage Collected Heap)。從記憶體回收角度來看,由於現在收集器基本都採用分代演演算法(為什麼要採用分代演演算法,常用的垃圾收集演演算法有哪些後面會進行介紹),所以堆中還以細分:新生代(Young/New)和老年代(Old/Tenure),新生代又可以劃分為Eden(伊甸園)空間、survivor(倖存區,其又可以分為from survivor和to survivor,也就是S0和S1)空間等。從記憶體分配的角度來看,執行緒共用的Java堆中可劃分出多個程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。不過無論如何劃分,都與存放內容無關,無論哪個區域,儲存的都仍然是物件範例,進一步劃分的是為了更好地回收記憶體,或更快地分配記憶體。
根據Java虛擬機器器規範的規定,Java堆可以處於物理上不連續的記憶體空間中,只邏輯上是連續的即可。.Java虛擬機器器中可以對堆進行擴充套件,可以通過-Xms 設定起始堆大小、通過-Xmx設定最大堆大小、通過-XX:NewSize設定新生代最小空間大小、通過 -XX:MaxNewSize設定新生代最大空間大小。如果在堆中沒有完成範例分配,並且地也無法再擴充套件時,將會拋OutOfMemoryError異常。
類載入子系統的作用
類載入器ClassLoader角色
載入.class檔案的方式
驗證(Verify)
目的在於確保Class檔案的位元組流中包含資訊符合當前虛擬機器器要求,保證被載入類的正確性,不會危害虛擬機器器自身安全。
主要包括四種驗證:檔案格式驗證、後設資料驗證、位元組碼驗證、符號參照驗證。
準備(Prepare)
解析(Resolve)
JVM支援兩種型別的類載入器,分別為引導類載入器(Bootstrap ClassLoader)和自定義類載入器User-Defined ClassLoader)。
從概念上講,自定義類載入器一般指的是程式中有開發人員自定義的一類類載入器,但是Java虛擬機器器規範卻沒有這麼定義,而是將所有派生於抽象類ClassLoader的類載入器都劃分為自定義類載入器。無論類載入器的型別如何劃分,在程式中我們最常見的類載入器始終只有3個,如下所示: