JVM的藝術—JAVA記憶體模型
*喜歡文章,動動手指點個贊 *
引言
親愛讀者你們好,關於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,而鎖是車。T1被BLOCKED在鎖(例子里的車)上,因為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寫了一個系列介紹內容,想要獲取更多內容,請關注公眾號:奇客時間