*喜歡文章,動動手指點個贊 *
引言
親愛讀者你們好,關于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寫了一個系列介紹內容,想要獲取更多內容,請關注公眾號:奇客時間
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/236339.html
標籤:Java
