JVM簡圖
運行時數據區簡圖
一、程序計數器(Program Counter Register)
1.程序計數器是什么?
程序計數器是JVM內存模型中的一部分,它可以看作是一個指針,指向當前線程所執行的字節碼指令的地址。每個線程在執行過程中都有自己的程序計數器,因此程序計數器是線程私有的,獨立于其他線程。
程序計數器不會OOM!!!
2.程序計數器的作用
-
指令執行:在每個線程執行字節碼指令時,程序計數器會存儲當前正在執行的字節碼指令的地址。如果是正在執行本地方法(native method),那么程序計數器的值將是undefined。
-
指令跳轉:在字節碼指令執行完畢后,程序計數器會自動更新為下一條要執行的字節碼指令的地址。通過這種方式,程序計數器可以確保字節碼指令按順序執行。
-
控制流管理:程序計數器幫助管理程序的控制流(如分支、循環、跳轉等)。通過更新程序計數器的值,可以實現各種控制流指令(如if、for循環、switch等)的跳轉邏輯。
-
多線程切換:由于Java是多線程的語言,每個線程都有自己獨立的程序計數器。當線程切換時,程序計數器會保存當前線程的執行位置,當線程再次被調度時,程序計數器會恢復到之前保存的位置,以確保線程可以繼續從正確的位置執行。
二、虛擬機棧(Java Virtual Machine Stack)
在Java虛擬機(JVM)中,每個線程在創建時都會創建一個虛擬機棧,虛擬機棧是每個線程私有的數據區,用于管理方法調用和執行。其內部保存一個個的棧幀(Stack Frame),對應著一次次的Java方法調用。每當一個線程調用一個方法時,JVM會為該方法創建一個新的棧幀(Stack Frame)并將其壓入虛擬機棧中,方法執行完畢后,棧幀會從棧中彈出。
**存在OOM,但是不需要垃圾回收**
如何設置棧大小
-Xss:一般默認大小為1024KB
單位為bytes,還可以使用KB/MB/GB單位進行設置
棧幀(Stack Frame)
1、JVM直接對Java棧的操作只有兩個,就是對棧幀的壓棧和出棧,遵循“先進后出”/“后進先出”原則。在一條活動線程中,一個時間點上,只會有一個活動的棧幀。
2、只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的,這個棧被稱為當前棧幀(Current Frame),與當前棧幀相對應的方法就是當前方法(CurrentMethod),定義這個方法的類就是當前類(CurrentClass)。
3、執行引擎運行的所有字節碼指令只針對當前棧幀進行操作。如果在該方法中調用了其他方法,對應的新的棧幀會被創建出來,放在棧的頂端,成為新的當前幀。
4、方法嵌套調用的極限次數,由棧的大小決定。超過大小時就會溢出OOM
1.棧幀的組成部分
- 局部變量表(Local Variable Array/Table)
- 操作數棧(Operand Stack)
- 動態鏈接(Dynamic Linking)
- 方法返回地址(Return Address)
- 附加信息(Additional Information)
2.詳細描述
1. 局部變量表(Local Variable Array/Table)
- 原理:
- 局部變量表是一個數組,用于存儲方法的局部變量,包括方法參數和方法內部定義的變量。 這些數據類型包括各類基本數據類型、對象引用,以及返回地址。
- 局部變量表是建立在線程的棧上,是線程私有數據,不存在數據安全問題。
- 局部變量表的容量大小,實在編譯期間確定下來的,并保存在方法的Code屬性的maximum local variables數據項中。在方法運行期間是不會改變局部變量表的大小的。
- 方法嵌套調用的極限次數,由棧的大小決定。參數和局部變量越多,使得局部變量表膨脹,棧幀就越大,就會導致嵌套調用次數減少。
- 表中的變量,只在當前方法調用中有效。方法調用結束后,隨著棧幀的銷毀,局部變量表隨之銷毀。
- 作用:為每個方法提供存儲和訪問局部變量的空間。局部變量通過索引進行訪問,索引從0開始。例如,
int a = 10;
中的a
就存儲在局部變量表中。 - 存儲信息:存儲了方法的參數和方法內部定義的局部變量。可以存儲各種數據類型,包括基本數據類型(int、float、long、double等)以及對象引用。
大概看一下局部變量表
1、上圖中,這里的參數名稱、參數類型中的cp_info#,就是符號名稱/符號引用,指的就是常量池中的內容。
2、以int 變量 a 為例,19、20 就對應了變量a和int類型
3、需要注意,非靜態方法的局部變量表中,第一個序號0一定為this,指向當前方法。靜態方法則沒有
4、序號是slot,32位的類型占用1個slot,64位的占用兩個slot,所以這里的序號都是1遞增。如果使用double變量,就會看到序號會+2
5、不足32位的按照32來算,其中byte、short、char、boolean都會被轉換為int來儲存
2. 操作數棧(Operand Stack)
- 原理:
- 操作數棧是一個LIFO棧,用于字節碼指令執行時的臨時存儲空間。
- 棧的最大深度在編譯期就定義好了。并保存在方法的Code屬性的max_stack數據中。
- 和局部變量表類似,在棧中,32位的類型占用1個深度,64位的占用兩個深度
- 作用:在方法執行過程中,用于保存中間計算結果、傳遞參數以及存儲返回值。例如,執行加法操作
i + j
時,會將i
和j
壓入操作數棧,執行完加法操作后,將結果存儲在操作數棧中。 - 存儲信息:方法執行過程中臨時存儲的操作數、中間計算結果。
舉例
3. 動態鏈接(Dynamic Linking)
- 原理:每個棧幀包含指向運行時常量池中,該棧幀所屬方法的引用,目的是為了支持當前方法的代碼能夠實現動態鏈接。
- 作用:
- Java源文件倍編輯成字節碼文件時,所有變量和方法引用,都作為符號引用,保存在常量池中
(在上面局部變量表中,有截圖)
- 當一個方法調用另外其他方法時,動態鏈接會將符號引用轉換為實際的方法內存地址。
- Java源文件倍編輯成字節碼文件時,所有變量和方法引用,都作為符號引用,保存在常量池中
(1)靜態鏈接和動態鏈接
- 靜態鏈接:
當一個字節碼文件被裝載進JVM內部時,如果被調用的目標方法在編譯期可知
,且運行期保持不變時
。這種情況下將調用方法的符號引用轉換為直接引用的過程稱之為靜態鏈接。 - 動態鏈接:
如果被調用的方法在編譯期無法被確定下來
,也就是說,只能夠在程序運行期
將調用方法的符號引用轉換為直接引用,由于這種引用轉換過程具備動態性,因此也就被稱之為動態鏈接。
(2)早期綁定和晚期綁定
對應的方法的綁定機制為:早期綁定(EarlyBinding)和晚期綁定(Late Binding)。綁定是一個字段、方法或者類在符號引用被替換為直接引用的過程,這僅僅發生一次。
- 早期綁定:
早期綁定就是指被調用的目標方法如果在編譯期可知,且運行期保持不變時即可將這個方法與所屬的類型進行綁定,這樣一來,由于明確了被調用的目標方法究竟是哪一個,因此也就可以使用靜態鏈接的方式將符號引用轉換為直接引用。 - 晚期綁定
如果被調用的方法在編譯期無法被確定下來,只能夠在程序運行期根據實際的類型綁定相關的方法,這種綁定方式也就被稱之為晚期綁定。
** 這里主要還是針對多態的 **
4. 方法返回地址(Return Address)
- 原理:
- 在方法調用時,返回地址會記錄調用方法的指令地址,以便方法返回時能找到正確的返回位置。
- 方法返回有兩類:正常完成、異常退出
- 正常返回時,會調用方法的下一條指令
- 異常退出時,需要通過異常表來確定,棧幀不保存相關信息
- 作用:方法執行完畢后,返回到調用該方法的地方繼續執行。這個地址一般是調用方法的下一條指令。
- 本質上,方法的退出就是當前棧幀出棧的過程。此時,需要恢復上層方法的局部變量表、操作數棧、將返回值壓入調用者棧幀的操作數棧、設置PC寄存器值等,讓調用者方法繼續執行下去。
- 正常完成出口和異常完成出口的區別在于:通過常完成出口退出的不會給他的上層調用者產生任何的返回值。
5. 附加信息(Additional Information)
- 原理:附加信息因JVM實現而異,包括棧幀的一些其他信息,比如調試信息和性能分析信息。
- 作用:為JVM提供更多的運行時信息支持,如異常處理信息、JVM優化信息等。
課后問答
- 舉例棧溢出的情況?(StackOverflowError)
- 調整棧大小,就能保證不出現溢出嗎?
- 分配的棧內存越大越好嗎?
- 垃圾回收是否會涉及到虛擬機棧?
- 方法中定義的局部變量是否線程安全?