JVM的藝術—JAVA記憶體模型

雕爺的架構之路發表於2020-12-17

*喜歡文章,動動手指點個贊 *

引言

親愛讀者你們好,關於jvm篇章的連載,前面三章講了類載入器,本篇文章將進入jvm領域的另一個知識點,java記憶體模型。徹底的瞭解java記憶體模型,是有必要的。只要掌握了java的記憶體模型,記憶體空間分為哪些區域,才能更好地理解,java是如何建立物件以及如何分配物件的空間。對後續的jvm調優打下堅實的基礎。而對於現在的網際網路行業來說,高併發,高可用已經必不可少,而學好jvm調優,不僅能在企業工作當中針對高併發場景下的系統進行優化,在日常對系統的錯誤排查、系統的優化也起著至關重要的作用。希望這篇文章能讓各位讀者學到真正的本領。同時也感謝大家的持續關注和認可。

一:JDK體系結構

JDK、JRE、JVM之間的關係

JDK:Java Development Kit(java開發工具包),包含JRE和開發工具包,例如javac、javah(生成實現本地方法所需的 C 標頭檔案和原始檔)。
JRE:Java Runtime Environment(java執行環境),包含JVM和類庫。
JVM:Java Virtual Machine(Java虛擬機器),負責執行符合規範的Class檔案。

Java語言的跨平臺特性

JVM所處的位置

(1)通常工作中所接觸的基本是Java庫和應用以及Java核心類庫,知道如何使用就可以了,但是歸根結底程式碼都是要編譯成class檔案由Java虛擬機器裝載執行,所產生的結果或者現象都可以通過Java虛擬機器的執行機制來解釋。一些相同的程式碼會由於虛擬機器的實現不同而產生不同結果。

(2)在Java平臺的結構中,可以看出,Java虛擬機器(JVM)處在核心的位置,是程式與底層作業系統和硬體無關的關鍵。它的下方是移植介面,移植介面由兩部分組成:介面卡和Java作業系統,其中依賴於平臺的部分稱為介面卡;JVM通過移植介面在具體的平臺和作業系統上實現;在JVM的上方是Java的基本類庫和擴充套件類庫以及它們的API, 利用Java API編寫的應用程式(application)和小程式(Java applet)可以在任何Java平臺上執行而無需考慮底層平臺,就是因為有Java虛擬機器(JVM)實現了程式與作業系統的分離,從而實現了Java的平臺無關性。

(3)對JVM規範的的抽象說明是一些概念的集合,它們已經在書《The Java Virtual Machine Specification》(《Java虛擬機器規範》)中被詳細地描述了;對JVM的具體實現要麼是軟體,要麼是軟體和硬體的組合,它已經被許多生產廠商所實現,並存在於多種平臺之上;執行Java程式的任務由JVM的執行期例項單個承擔。

(4)JVM可以由不同的廠商來實現。由於廠商的不同必然導致JVM在實現上的一些不同,像國內就有著名的TaobaoVM;然而JVM還是可以實現跨平臺的特性,這就要歸功於設計JVM時的體系結構了。

(5)JVM在它的生存週期中有一個明確的任務,那就是裝載位元組碼檔案,一旦位元組碼進入虛擬機器,它就會被直譯器解釋執行,或者是被即時程式碼發生器有選擇的轉換成機器碼執行,即Java程式被執行。因此當Java程式啟動的時候,就產生JVM的一個例項;當程式執行結束的時候,該例項也跟著消失了。

Class位元組碼

編譯後被Java虛擬機器所執行的程式碼使用了一種平臺中立(不依賴於特定硬體及作業系統的)的二進位制格式來表示,並且經常(但並非絕對)以檔案的形式儲存,因此這種格式被稱為Class檔案格式。Class檔案格式中精確地定義了類與介面的表示形式,包括在平臺相關的目標檔案格式中一些細節上的慣例,
正如概念所說,Java為了能夠實現平臺無關性,制定了一套自己的二進位制格式,並經常以檔案的方式儲存,稱為Class檔案。這樣在不同平臺上,只要都安裝了Java虛擬機器,具備Java執行環境[JRE],那麼都可以執行相同的Class檔案。

上圖描述了Java程式執行的一個全過程,也可以看出Java平臺由Java虛擬機器和Java應用程式介面搭建,Java語言則是進入這個平臺的通道,用Java語言編寫並編譯的程式可以執行在這個平臺上。
由Java原始檔編譯生成位元組碼檔案,這個過程非常複雜,學過《編譯原理》的朋友都知道必須經過詞法分析、語法分析、語義分析、中間程式碼生成、程式碼優化等;同樣的,Java原始檔到位元組碼的生成也想要經歷這些步驟。Javac編譯器的最後任務就是呼叫con.sun.tools.javac.jvm.Gen類將這課語法樹編譯為Java位元組碼檔案。
其實,所謂的編譯位元組碼,無非就是將符合Java語法規範的Java程式碼轉化為符合JVM規範的位元組碼檔案。JVM的架構模型是基於棧的,大部分都需要通過棧來完成。
位元組碼結構比較特殊,其內部不包含任何的分隔符,無法人工區分段落(位元組碼檔案本身就是給機器讀的),所以無論是位元組順序、數量都是有嚴格規定的,所有16位、32位、64位長度的資料都將構造成2個、4個、8個-----8位位元組單位來表示,多位元組資料項總是按照Big-endian順序(高位位元組在地址的最低位,地位位元組在地址的最高位)來進行儲存。
參考《Java虛擬機器規範 Java SE7版》的描述,每一個位元組碼其實都對應著全域性唯一的一個類或者介面的定義資訊。位元組碼檔案才用的是一種類似於C語言結構體的偽結構來描述位元組碼檔案格式。位元組碼檔案中對應的“基本型別”u1,u2,u4,u8分別表示無符號1、2、4、8個位元組。

Class檔案----總體格式

值得一提的是,一個有效的class位元組碼檔案的前4個位元組為0xCAFEBABE,都是固定的,被稱為“魔術”,即magic。它就是JVM用於校驗所讀取的目標檔案是否是一個有效且合法的位元組碼檔案。由此可見,JVM並不是通過判斷檔案字尾名的方式來校驗,以防止人為手動修改。

JVM底層架構圖

上面這張圖,是本人花了很多心思總結出來的,基本涵蓋了java記憶體模型的結構。今天奉上。這篇文章會把上面這張圖講清楚。

執行時資料區:

1,堆

Java堆在虛擬機器啟動的時候被建立,Java堆主要用來為類例項物件和陣列分配記憶體。Java虛擬機器規範並沒有規定物件在堆中的形式。
在Java中,堆被劃分成兩個不同的區域:新生代( Young )、老年代( Old );這也就是JVM採用的“分代收集演算法”,簡單說,就是針對不同特徵的java物件採用不同的 策略實施存放和回收,自然所用分配機制和回收演算法就不一樣。新生代( Young ) 又被劃分為三個區域:Eden、From Survivor、To Survivor。

分代收集演算法:採用不同演算法處理[存放和回收]Java瞬時物件和長久物件。大部分Java物件都是瞬時物件,朝生夕滅,存活很短暫,通常存放在Young新生代,採用複製演算法對新生代進行垃圾回收。老年代物件的生命週期一般都比較長,極端情況下會和JVM生命週期保持一致;通常採用標記-壓縮演算法對老年代進行垃圾回收。
這樣劃分的目的是為了使JVM能夠更好的管理堆記憶體中的物件,包括記憶體的分配以及回收。
  Java堆可能發生如下異常情況:如果實際所需的堆超過了自動記憶體管理系統能提供的最大容量,那Java虛擬機器將會丟擲一個OutOfMemoryError異常。簡稱(OOM)。

堆大小 = 新生代 + 老年代。堆的大小可通過引數–Xms(堆的初始容量)、-Xmx(堆的最大容量) 來指定。

其中,新生代 ( Young ) 被細分為 Eden 和 兩個 Survivor 區域,這兩個 Survivor 區域分別被命名為 from 和 to,以示區分。預設的,Edem : from : to = 8 : 1 : 1 。(可以通過引數 –XX:SurvivorRatio 來設定 。

即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。

JVM 每次只會使用 Eden 和其中的一塊 Survivor 區域來為物件服務,所以無論什麼時候,總是有一塊 Survivor 區域是空閒著的。

新生代實際可用的記憶體空間為 9/10 ( 即90% )的新生代空間。

java堆是GC垃圾回收的主要區域。 GC分為兩種: Minor GC、Full GC(也叫做Major GC)

Minor GC(簡稱GC)
Minor GC是發生在新生代中的垃圾收集動作, 所採用的是複製演算法。
GC一般為堆空間某個區發生了垃圾回收,
新生代(Young)幾乎是所有java物件出生的地方。即java物件申請的記憶體以及存放都是在這個地方。java中的大部分物件通常不會長久的存活, 具有朝生夕死的特點。
當一個物件被判定為“死亡”的時候, GC就有責任來回收掉這部分物件的記憶體空間。
新生代是收集垃圾的頻繁區域。

2,方法區(元空間)

方法區在虛擬機器啟動的時候被建立,它儲存了每一個類的結構資訊,例如執行時常量池、欄位和方法資料、建構函式和普通方法的位元組碼內容、還包括在類、例項、介面初始化時用到的特殊方法。
方法區可能發生如下異常情況: 如果方法區的記憶體空間不能滿足記憶體分配請求,那Java虛擬機器將丟擲一個OutOfMemoryError異常.

3,JVM棧空間

每個Java虛擬機器執行緒都有自己的Java虛擬機器棧。Java虛擬機器棧用來存放棧幀,而棧幀主要包括了:區域性變數表、運算元棧、動態連結。Java虛擬機器棧允許被實現為固定大小或者可動態擴充套件的記憶體大小。
Java虛擬機器使用區域性變數表來完成方法呼叫時的引數傳遞。區域性變數表的長度在編譯期已經決定了並儲存於類和介面的二進位制表示中,一個區域性變數可以儲存一個型別為boolean、byte、char、short、float、reference和returnAddress的資料,兩個區域性變數可以儲存一個型別為long和double的資料。
  Java虛擬機器提供一些位元組碼指令來從區域性變數表或者物件例項的欄位中複製常量或變數值到運算元棧中,也提供了一些指令用於從運算元棧取走資料、運算元據和把操作結果重新入棧。在方法呼叫的時候,運算元棧也用來準備呼叫方法的引數以及接收方法返回結果。
  每個棧幀中都包含一個指向執行時常量區的引用支援當前方法的動態連結。在Class檔案中,方法呼叫和訪問成員變數都是通過符號引用來表示的,動態連結的作用就是將符號引用轉化為實際方法的直接引用或者訪問變數的執行是記憶體位置的正確偏移量。
總的來說,Java虛擬機器棧是用來存放區域性變數和過程結果的地方。
Java虛擬機器棧可能發生如下異常情況: 如果Java虛擬機器棧被實現為固定大小記憶體,執行緒請求分配的棧容量超過Java虛擬機器棧允許的最大容量時,Java虛擬機器將會丟擲一個StackOverflowError異常。
如果Java虛擬機器棧被實現為動態擴充套件記憶體大小,並且擴充套件的動作已經嘗試過,但是目前無法申請到足夠的記憶體去完成擴充套件,或者在建立新的執行緒時沒有足夠的記憶體去建立對應的虛擬機器棧,那Java虛擬機器將會丟擲一個OutOfMemoryError異常。

1.符號引用(Symbolic References):

  符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能夠無歧義的定位到目標即可。例如,在Class檔案中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等型別的常量出現。符號引用與虛擬機器的記憶體佈局無關,引用的目標並不一定載入到記憶體中。在Java中,一個java類將會編譯成一個class檔案。在編譯時,java類並不知道所引用的類的實際地址,因此只能使用符號引用來代替。比如org.simple.People類引用了org.simple.Language類,在編譯時People類並不知道Language類的實際記憶體地址,因此只能使用符號org.simple.Language(假設是這個,當然實際中是由類似於CONSTANT_Class_info的常量來表示的)來表示Language類的地址。各種虛擬機器實現的記憶體佈局可能有所不同,但是它們能接受的符號引用都是一致的,因為符號引用的字面量形式明確定義在Java虛擬機器規範的Class檔案格式中。

2.直接引用:

直接引用可以是

(1)直接指向目標的指標(比如,指向“型別”【Class物件】、類變數、類方法的直接引用可能是指向方法區的指標)

(2)相對偏移量(比如,指向例項變數、例項方法的直接引用都是偏移量)

(3)一個能間接定位到目標的控制程式碼

直接引用是和虛擬機器的佈局相關的,同一個符號引用在不同的虛擬機器例項上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經被載入入記憶體中了。

4,本地方法棧

對於一個執行中的Java程式而言,它還可能會用到一些跟本地方法相關的資料區。當某個執行緒呼叫一個本地方法時,它就進入了一個全新的並且不再受虛擬機器限制的世界。本地方法可以通過本地方法介面來訪問虛擬機器的執行時資料區,但不止如此,它還可以做任何它想做的事情。

  本地方法本質上時依賴於實現的,虛擬機器實現的設計者們可以自由地決定使用怎樣的機制來讓Java程式呼叫本地方法。

  任何本地方法介面都會使用某種本地方法棧。當執行緒呼叫Java方法時,虛擬機器會建立一個新的棧幀並壓入Java棧。然而當它呼叫的是本地方法時,虛擬機器會保持Java棧不變,不再線上程的Java棧中壓入新的幀,虛擬機器只是簡單地動態連線並直接呼叫指定的本地方法。

  如果某個虛擬機器實現的本地方法介面是使用C連線模型的話,那麼它的本地方法棧就是C棧。當C程式呼叫一個C函式時,其棧操作都是確定的。傳遞給該函式的引數以某個確定的順序壓入棧,它的返回值也以確定的方式傳回撥用者。同樣,這就是虛擬機器實現中本地方法棧的行為。

  很可能本地方法介面需要回撥Java虛擬機器中的Java方法,在這種情況下,該執行緒會儲存本地方法棧的狀態並進入到另一個Java棧。

  下圖描繪了這樣一個情景,就是當一個執行緒呼叫一個本地方法時,本地方法又回撥虛擬機器中的另一個Java方法。

  這幅圖展示了JAVA虛擬機器內部執行緒執行的全景圖。一個執行緒可能在整個生命週期中都執行Java方法,操作它的Java棧;或者它可能毫無障礙地在Java棧和本地方法棧之間跳轉。 

該執行緒首先呼叫了兩個Java方法,而第二個Java方法又呼叫了一個本地方法,這樣導致虛擬機器使用了一個本地方法棧。假設這是一個C語言棧,其間有兩個C函式,第一個C函式被第二個Java方法當做本地方法呼叫,而這個C函式又呼叫了第二個C函式。之後第二個C函式又通過本地方法介面回撥了一個Java方法(第三個Java方法),最終這個Java方法又呼叫了一個Java方法(它成為圖中的當前方法)。

Navtive 方法是 Java 通過 JNI 直接呼叫本地 C/C++ 庫,可以認為是 Native 方法相當於 C/C++ 暴露給 Java 的一個介面,Java 通過呼叫這個介面從而呼叫到 C/C++ 方法。當執行緒呼叫 Java 方法時,虛擬機器會建立一個棧幀並壓入 Java 虛擬機器棧。然而當它呼叫的是 native 方法時,虛擬機器會保持 Java 虛擬機器棧不變,也不會向 Java 虛擬機器棧中壓入新的棧幀,虛擬機器只是簡單地動態連線並直接呼叫指定的 native 方法。

5,程式計數器

程式計數器是一個記錄著當前執行緒所執行的位元組碼的行號指示器。

  JAVA程式碼編譯後的位元組碼在未經過JIT(實時編譯器)編譯前,其執行方式是通過“位元組碼直譯器”進行解釋執行。簡單的工作原理為直譯器讀取裝載入記憶體的位元組碼,按照順序讀取位元組碼指令。讀取一個指令後,將該指令“翻譯”成固定的操作,並根據這些操作進行分支、迴圈、跳轉等流程。

  從上面的描述中,可能會產生程式計數器是否是多餘的疑問。因為沿著指令的順序執行下去,即使是分支跳轉這樣的流程,跳轉到指定的指令處按順序繼續執行是完全能夠保證程式的執行順序的。假設程式永遠只有一個執行緒,這個疑問沒有任何問題,也就是說並不需要程式計數器。但實際上程式是通過多個執行緒協同合作執行的。

  首先我們要搞清楚JVM的多執行緒實現方式。JVM的多執行緒是通過CPU時間片輪轉(即執行緒輪流切換並分配處理器執行時間)演算法來實現的。也就是說,某個執行緒在執行過程中可能會因為時間片耗盡而被掛起,而另一個執行緒獲取到時間片開始執行。當被掛起的執行緒重新獲取到時間片的時候,它要想從被掛起的地方繼續執行,就必須知道它上次執行到哪個位置,在JVM中,通過程式計數器來記錄某個執行緒的位元組碼執行位置。因此,程式計數器是具備執行緒隔離的特性,也就是說,每個執行緒工作時都有屬於自己的獨立計數器。

程式計數器的特點

  1.執行緒隔離性,每個執行緒工作時都有屬於自己的獨立計數器。
  2.執行java方法時,程式計數器是有值的,且記錄的是正在執行的位元組碼指令的地址(參考上一小節的描述)。
  3.執行native本地方法時,程式計數器的值為空(Undefined)。因為native方法是java通過JNI直接呼叫本地C/C++庫,可以近似的認為native方法相當於C/C++暴露給java的一個介面,java通過呼叫這個介面從而呼叫到C/C++方法。由於該方法是通過C/C++而不是java進行實現。那麼自然無法產生相應的位元組碼,並且C/C++執行時的記憶體分配是由自己語言決定的,而不是由JVM決定的。

​ 4.程式計數器佔用記憶體很小,在進行JVM記憶體計算時,可以忽略不計。

  5.程式計數器,是唯一一個在java虛擬機器規範中沒有規定任何OutOfMemoryError的區域。

6,執行緒棧

執行緒堆疊也稱執行緒呼叫堆疊,是虛擬機器中執行緒(包括鎖)狀態的一個瞬間狀態的快照,即系統在某一個時刻所有執行緒的執行狀態,包括每一個執行緒的呼叫堆疊,鎖的持有情況。雖然不同的虛擬機器列印出來的格式有些不同,但是執行緒堆疊的資訊都包含:

1、執行緒名字,id,執行緒的數量等。

2、執行緒的執行狀態,鎖的狀態(鎖被哪個執行緒持有,哪個執行緒在等待鎖等)

3、呼叫堆疊(即函式的呼叫層次關係)呼叫堆疊包含完整的類名,所執行的方法,原始碼的行數。

因為執行緒棧是瞬時快照包含執行緒狀態以及呼叫關係,所以藉助堆疊資訊可以幫助分析很多問題,比如執行緒死鎖,鎖爭用,死迴圈,識別耗時操作等等。執行緒棧是瞬時記錄,所以沒有歷史訊息的回溯,一般我們都需要結合程式的日誌進行跟蹤,一般執行緒棧能分析如下效能問題:

1、系統無緣無故的cpu過高

2、系統掛起,無響應

3、系統執行越來越慢

4、效能瓶頸(如無法充分利用cpu等)

5、執行緒死鎖,死迴圈等

6、由於執行緒數量太多導致的記憶體溢位(如無法建立執行緒等)

執行緒棧狀態

執行緒棧狀態有如下幾種

1、NEW

2、RUNNABLE

3、BLOCKED

4、WAITING

5、TIMED_WAITING

6、TERMINATED

下面依次對6種執行緒棧狀態進行介紹。

1、NEW

執行緒剛剛被建立,也就是已經new過了,但是還沒有呼叫start()方法,這個狀態我們使用jstack進行執行緒棧dump的時候基本看不到,因為是執行緒剛建立時候的狀態。

2、RUNNABLE

從虛擬機器的角度看,執行緒正在執行狀態,狀態是執行緒正在正常執行中, 當然可能會有某種耗時計算/IO等待的操作/CPU時間片切換等, 這個狀態下發生的等待一般是其他系統資源, 而不是鎖, Sleep等。

處於RUNNABLE狀態的執行緒是不是一定會消耗cpu呢,不一定,像socket IO操作,執行緒正在從網路上讀取資料,儘管執行緒狀態RUNNABLE,但實際上網路io,執行緒絕大多數時間是被掛起的,只有當資料到達後,執行緒才會被喚起,掛起發生在原生程式碼(native)中,虛擬機器根本不一致,不像顯式的呼叫sleep和wait方法,虛擬機器才能知道執行緒的真正狀態,但在原生程式碼中的掛起,虛擬機器無法知道真正的執行緒狀態,因此一概顯示為RUNNABLE。

3、BLOCKED

執行緒處於阻塞狀態,正在等待一個monitor lock。通常情況下,是因為本執行緒與其他執行緒公用了一個鎖。其他線上程正在使用這個鎖進入某個synchronized同步方法塊或者方法,而本執行緒進入這個同步程式碼塊也需要這個鎖,最終導致本執行緒處於阻塞狀態。

真實生活例子:

今天你要去阿里面試。這是你夢想的工作,你已經盯著它多年了。你早上起來,準備好,穿上你最好的外衣,對著鏡子打理好。當你走進車庫發現你的朋友已經把車開走了。在這個場景,你只有一輛車,所以怎麼辦?在真實生活中,可能會打架搶車。 現在因為你朋友把車開走了你被BLOCKED了。你不能去參加面試。

這就是BLOCKED狀態。用技術術語講,你是執行緒T1,你朋友是執行緒T2,而鎖是車。T1BLOCKED在鎖(例子裡的車)上,因為T2已經獲取了這個鎖。

4、WAITING

這個狀態下是指執行緒擁有了某個鎖之後, 呼叫了他的wait方法, 等待其他執行緒/鎖擁有者呼叫 notify / notifyAll一遍該執行緒可以繼續下一步操作, 這裡要區分 BLOCKED 和 WATING 的區別, 一個是在臨界點外面等待進入, 一個是在理解點裡面wait等待別人notify, 執行緒呼叫了join方法 join了另外的執行緒的時候, 也會進入WAITING狀態, 等待被他join的執行緒執行結束,處於waiting狀態的執行緒基本不消耗CPU。

真實生活例子:

再看下幾分鐘後你的朋友開車回家了,鎖(車)就被釋放了,現在你意識到快到面試時間了,而開車過去很遠。所以你拼命地踩油門。限速120KM/H而你以160KM/H的速度在開。很不幸,一個交警發現你超速了,讓你停到路邊。現在你進入了WAITING狀態。你停下車坐在那等著交警過來檢查開罰單然後給你放行。基本上,你只有等他讓你走(你沒法開車逃),你被卡在WAITING狀態了。

用技術術語來講,你是執行緒T1而交警是執行緒T2。你釋放你的鎖(例子中你停下了車),並進入WAITING狀態,直到警察(例子中T2)讓你走,你陷入了WAITING狀態。

5、TIMED_WAITING

該執行緒正在等待,通過使用了 sleep, wait, join 或者是 park 方法。(這個與 WAITING 不同是通過方法引數指定了最大等待時間,WAITING 可以通過時間或者是外部的變化解除),執行緒等待指定的時間。

真實生活例子:

儘管這次面試過程充滿戲劇性,但你在面試中做的非常好,驚豔了所有人並獲得了高薪工作。你回家告訴你的鄰居你的新工作並表達你激動的心情。你的朋友告訴你他也在同一個辦公樓裡工作。他建議你坐他的車去上班。你想這不錯。所以去阿里上班的第一天,你走到你鄰居的房子,在他的房子前停好你的車。你等了他10分鐘,但你的鄰居沒有出現。你然後繼續開自己的車去上班,這樣你不會在第一天就遲到。這就是TIMED_WAITING.

用技術術語來解釋,你是執行緒T1而你的鄰居是執行緒T2。你釋放了鎖(這裡是停止開車)並等了足足10分鐘。如果你的鄰居T2沒有來,你繼續開車(老司機注意車速,其他乘客記得買票)。

6、TERMINATED

執行緒終止,同樣我們在使用jstack進行執行緒dump的時候也很少看到該狀態的執行緒棧。

1.區域性變數表

區域性變數表(Local Variable Table)是一組變數值儲存空間,用於存放方法引數和方法內定義的區域性變數。區域性變數表的容量以變數槽(Variable Slot)為最小單位,Java虛擬機器規範並沒有定義一個槽所應該佔用記憶體空間的大小,但是規定了一個槽應該可以存放一個32位以內的資料型別。

在Java程式編譯為Class檔案時,就在方法的Code屬性中的max_locals資料項中確定了該方法所需分配的區域性變數表的最大容量。(最大Slot數量)

一個區域性變數可以儲存一個型別為boolean、byte、char、short、int、float、reference和returnAddress型別的資料。reference型別表示對一個物件例項的引用。returnAddress型別是為jsr、jsr_w和ret指令服務的,目前已經很少使用了。

虛擬機器通過索引定位的方法查詢相應的區域性變數,索引的範圍是從0~區域性變數表最大容量。如果Slot是32位的,則遇到一個64位資料型別的變數(如long或double型),則會連續使用兩個連續的Slot來儲存。

2.運算元棧

運算元棧(Operand Stack)也常稱為操作棧,它是一個後入先出棧(LIFO)。同區域性變數表一樣,運算元棧的最大深度也在編譯的時候寫入到方法的Code屬性的max_stacks資料項中。

運算元棧的每一個元素可以是任意Java資料型別,32位的資料型別佔一個棧容量,64位的資料型別佔2個棧容量,且在方法執行的任意時刻,運算元棧的深度都不會超過max_stacks中設定的最大值。

當一個方法剛剛開始執行時,其運算元棧是空的,隨著方法執行和位元組碼指令的執行,會從區域性變數表或物件例項的欄位中複製常量或變數寫入到運算元棧,再隨著計算的進行將棧中元素出棧到區域性變數表或者返回給方法呼叫者,也就是出棧/入棧操作。一個完整的方法執行期間往往包含多個這樣出棧/入棧的過程。

3.動態連線

在一個class檔案中,一個方法要呼叫其他方法,需要將這些方法的符號引用轉化為其在記憶體地址中的直接引用,而符號引用存在於方法區中的執行時常量池。

Java虛擬機器棧中,每個棧幀都包含一個指向執行時常量池中該棧所屬方法的符號引用,持有這個引用的目的是為了支援方法呼叫過程中的動態連線(Dynamic Linking)

這些符號引用一部分會在類載入階段或者第一次使用時就直接轉化為直接引用,這類轉化稱為靜態解析。另一部分將在每次執行期間轉化為直接引用,這類轉化稱為動態連線。

4.靜態連結

靜態連結的過程就已經把要連結的內容已經連結到了生成的可執行檔案中,就算你在去把靜態庫刪除也不會影響可執行程式的執行;而動態連結這個過程卻沒有把內容連結進去,而是在執行的過程中,再去找要連結的內容,生成的可執行檔案中並沒有要連結的內容,所以當你刪除動態庫時,可執行程式就不能執行。

通俗解釋:靜態連線庫就是把(lib)檔案中用到的函式程式碼直接連結進目標程式,程式執行的時候不再需要其它的庫檔案;動態連結就是把呼叫的函式所在檔案模組(DLL)和呼叫函式在檔案中的位置等資訊連結進目標程式,程式執行的時候再從DLL中尋找相應函式程式碼,因此需要相應DLL檔案的支援。

這篇內容主要介紹一下圖中的概念。下篇文章我會把這些概念串起來,比如說建立物件的過程,記憶體空間是怎麼工作的。感謝大家的持續關注。

另外我在我的公眾號內,針對JVM寫了一個系列介紹內容,想要獲取更多內容,請關注公眾號:奇客時間

相關文章