JVM-虚拟机栈详解 附面试高频题 (手画多图)!!!深入浅出,绝对值得收藏哈!!!
受多種情況的影響,又開始看JVM 方面的知識。
1、Java 實在過于內(nèi)卷,沒法不往深了學(xué)。
2、面試題問的多,被迫學(xué)習(xí)。
3、純粹的好奇。
很喜歡一句話:“八小時內(nèi)謀生活,八小時外謀發(fā)展。”
— 望別日與君相見時,君已有所成。
共勉
來源:b站說唱新時代中 剁椒沙丁魚隊《世界以痛吻我》中歌手:于貞
作者:博主制作
原因:人美聲甜可愛可辣可酷(我才不會說是我喜歡的) 🤸?♂?🤸?♂?🤸?♂?
JVM-虛擬機(jī)棧詳解 附面試高頻題 (手畫多圖)
- 一、虛擬機(jī)棧概述
- 二、棧幀
- 2.1、棧與棧楨:
- 2.2、棧幀概述
- 2.3、想一想我們遇到過哪些與棧相關(guān)的異常?
- 2.4、設(shè)置棧內(nèi)存大小
- 2.5、局部變量表
- 概述:
- 局部變量的存放 Slot(變量槽)
- 靜態(tài)變量與局部變量的對比
- 2.6、操作數(shù)棧
- 2.7、動態(tài)鏈接
- 概述
- 鏈接
- 2.8、方法返回地址
- 異常表:
- 2.9、一些附加信息
- 面試
- 自言自語
一、虛擬機(jī)棧概述
先給大家來看一下 運行時數(shù)據(jù)區(qū)的圖示👇
如果大家沒咋了解Java的內(nèi)存結(jié)構(gòu),就常會粗粒度地將JVM中的內(nèi)存區(qū)理解為僅有Java堆(heap)和Java戰(zhàn)(stack)?為什么?🤳🧐
首先棧是運行時的單位,而堆是存儲的單位
- 棧解決程序的運行問題,即程序如何執(zhí)行,或者說如何處理數(shù)據(jù)。
- 堆解決的是數(shù)據(jù)存儲的問題,即數(shù)據(jù)怎么放,放哪里
不過今天我們討論的是虛擬機(jī)棧。堆的文章之后才更👨?💻。
虛擬機(jī)棧:java虛擬機(jī)棧是線程私有的,他與線程的聲明周期同步。虛擬機(jī)棧描述的是java方法執(zhí)行的內(nèi)存模型,每個方法執(zhí)行都會創(chuàng)建一個棧幀,棧幀包含局部變量表、操作數(shù)棧、動態(tài)連接、方法出口等。
注意: 🏂
二、棧幀
2.1、棧與棧楨:
每一個方法的執(zhí)行到執(zhí)行完成,對應(yīng)著一個棧幀在虛擬機(jī)中從入棧到出棧的過程。 👨?🚀
1、java虛擬機(jī)棧棧頂?shù)臈褪钱?dāng)前執(zhí)行方法的棧幀,PC寄存器會指向該地址。👇
2、當(dāng)這個方法調(diào)用其他方法的時候就會創(chuàng)建一個新的棧幀,這個新的棧幀會被方法Java虛擬機(jī)棧的棧頂,變?yōu)楫?dāng)前的活動棧,在當(dāng)前只有當(dāng)前活動棧的本地變量才能被使用,
3、當(dāng)這個棧幀所有指令都完成的時候,這個棧幀被移除,之前的棧幀變?yōu)榛顒訔?#xff0c;前面移除棧幀的返回值變?yōu)檫@個棧幀的一個操作數(shù)。
2.2、棧幀概述
棧幀(Stack Frame)是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機(jī)運行時數(shù)據(jù)區(qū)的虛擬機(jī)棧(Virtual Machine Stack)的棧元素。每個棧幀都包含了:
👩?💻
在編譯代碼的時,棧幀中需要多大的局部變量表,多深的操作數(shù)棧都已經(jīng)完全確定了,并且寫入到了方法表的Code屬性中,因此一個棧幀需要分配多少內(nèi)存,不會受到程序運行期變量數(shù)據(jù)的影響,而僅僅取決于具體虛擬機(jī)的實現(xiàn)。
如下圖:
左邊是通過javap -v 類名的.class 命令反編譯出來的。注意啊,得在生成目標(biāo)文件夾目錄下👇
我們通過上圖可以看到,在編譯過程中,已經(jīng)給每個棧楨分配好了 操作數(shù)棧 的深度啊,局部變量表的大小等等。
局部變量表是4的原因:雖然我們在這個方法中只定義了a,b,c 三個局部變量,但是大家還記得 this嗎,你沒有想錯,確實在這個局部變量表中,第一個是this。更深層次解釋不了,技術(shù)不夠。😂。到局部變量部分帶大家一起看。
在這里插一句哈,如果大家不熟悉這種命令行去反編譯的話,在這里介紹一個idea 的插件。
放心哈,那插件作者肯定沒給我打錢,我是感覺真的挺可的,對于我們新手學(xué)習(xí)這方面。
名字: jclasslib Bytecode Viewer
用法
我們將一個類編譯完后,兄弟們,編譯沒有問題吧。不行,感覺還是要貼出來哈。
編譯完成之后。我們打開 菜單中 -->view 選項。
里面的具體的東東靠大家慢慢發(fā)掘了哈,我們還是回歸正文啦。給大家個Oracle 的JVM 官方規(guī)范。方便指令的查找解釋哈。
2.3、想一想我們遇到過哪些與棧相關(guān)的異常?
Java 虛擬機(jī)規(guī)范允許Java棧的大小是動態(tài)的或者是固定不變的。 🏄?♂?
1、如果采用固定大小的Java虛擬機(jī)棧,那每一個線程的Java虛擬機(jī)棧容量可以在線程創(chuàng)建的時候獨立選定。如果線程請求分配的棧容量超過Java虛擬機(jī)棧允許的最大容量,Java虛擬機(jī)將會拋出一個``StackoverflowError `異常。(棧溢出)
舉個栗子:相信大家肯定學(xué)過遞歸算法,如果它一直沒有出口,結(jié)果就是棧溢出。🚣?♂?
2、如果Java虛擬機(jī)棧可以動態(tài)擴(kuò)展,并且在嘗試擴(kuò)展的時候無法申請到足夠的內(nèi)存,或者在創(chuàng)建新的線程時沒有足夠的內(nèi)存去創(chuàng)建對應(yīng)的虛擬機(jī)棧,那Java虛擬機(jī)將會拋出一個 OutOfMemoryError 異常。(也就是內(nèi)存溢出異常OOM)
2.4、設(shè)置棧內(nèi)存大小
剛剛大家也看到了,我們可以使用參數(shù) -Xss選項來設(shè)置線程的最大棧空間,棧的大小直接決定了函數(shù)調(diào)用的最大可達(dá)深度。
-Xss1m // m 是Mb -Xss1k // k 是Kb2.5、局部變量表
概述:
局部變量表:Local Variables,被稱之為局部變量數(shù)組或本地變量表。定義為一個數(shù)字?jǐn)?shù)組,主要用于存儲方法參數(shù)和定義在方法體內(nèi)的局部變量這些數(shù)據(jù)類型包括各類基本數(shù)據(jù)類型、對象引用(reference),以及returnAddress類型。
局部變量表是線程私有的。
局部變量表所需的容量大小是在編譯期確定下來的,并保存在方法的Code屬性的maximum local variables數(shù)據(jù)項中。在方法運行期間是不會改變局部變量表的大小的。
局部變量表中的變量只在當(dāng)前方法調(diào)用中有效。在方法執(zhí)行時,虛擬機(jī)通過使用局部變量表完成參數(shù)值到參數(shù)變量列表的傳遞過程。當(dāng)方法調(diào)用結(jié)束后,隨著方法棧幀的銷毀,局部變量表也會隨之銷毀。
👨?💻👨?🚀🤹?♂?🤽?♂?🏌??♂?🐱?🚀🐱?🐉🎊🧬🚀🛫🚢🛸🚤?🌈🌊
悄咪咪試一波小表情,課間休息會,怕看疲憊了,就放棄繼續(xù)看下去啦哈。🤗
局部變量的存放 Slot(變量槽)
參數(shù)值的存放總是在局部變量數(shù)組的index0開始,到數(shù)組長度-1的索引結(jié)束。🤭
局部變量表,最基本的存儲單元是Slot(變量槽)局部變量表中存放編譯期可知的各種基本數(shù)據(jù)類型(8種),引用類型(reference),returnAddress類型的變量。
在局部變量表里,32位以內(nèi)的類型只占用一個slot(包括returnAddress類型),64位的類型(long和double)占用兩個slot。
JVM會為局部變量表中的每一個Slot都分配一個訪問索引,通過這個索引即可成功訪問到局部變量表中指定的局部變量值。
靜態(tài)變量與局部變量的對比
變量的分類:😜
- 按數(shù)據(jù)類型分:基本數(shù)據(jù)類型、引用數(shù)據(jù)類型
- 按類中聲明的位置分:成員變量(類變量,實例變量)、局部變量
- 類變量:linking的paper階段,給類變量默認(rèn)賦值,init階段給類變量顯示賦值即靜態(tài)代碼塊
- 實例變量:隨著對象創(chuàng)建,會在堆空間中分配實例變量空間,并進(jìn)行默認(rèn)賦值
- 局部變量:在使用前必須進(jìn)行顯式賦值,不然編譯不通過。
🎅
我們知道類變量表有兩次初始化的機(jī)會,第一次是在“準(zhǔn)備階段”,執(zhí)行系統(tǒng)初始化,對類變量設(shè)置零值,另一次則是在“初始化”階段,賦予程序員在代碼中定義的初始值。
和類變量初始化不同的是,局部變量表不存在系統(tǒng)初始化的過程。這意味著如果創(chuàng)建了局部變量,并且在使用前不對它進(jìn)行顯示賦值,那么將無法通過編譯。
在棧幀中,與性能調(diào)優(yōu)關(guān)系最為密切的部分就是前面提到的局部變量表。在方法執(zhí)行時,虛擬機(jī)使用局部變量表完成方法的傳遞。
🛀
局部變量表中的變量也是重要的垃圾回收根節(jié)點,只要被局部變量表中直接或間接引用的對象都不會被回收。
2.6、操作數(shù)棧
1、每一個獨立的棧幀除了包含局部變量表以外,還包含一個后進(jìn)先出(Last - In - First -Out)的 操作數(shù)棧,也可以稱之為 表達(dá)式棧(Expression Stack)🤪
操作數(shù)棧,在方法執(zhí)行過程中,根據(jù)字節(jié)碼指令,往棧中寫入數(shù)據(jù)或提取數(shù)據(jù),即入棧(push)和 出棧(pop)
- 某些字節(jié)碼指令將值壓入操作數(shù)棧,其余的字節(jié)碼指令將操作數(shù)取出棧。使用它們后再把結(jié)果壓入棧
- 比如:執(zhí)行復(fù)制、交換、求和等操作
🤦?♂?
2、操作數(shù)棧,主要用于保存計算過程的中間結(jié)果,同時作為計算過程中變量臨時的存儲空間。
3、每一個操作數(shù)棧都會擁有一個明確的棧深度用于存儲數(shù)值,其所需的最大深度在編譯期就定義好了,保存在方法的Code屬性中,為maxstack的值。
4、操作數(shù)棧的每一個元素可以是任意Java數(shù)據(jù)類型,32位的數(shù)據(jù)類型占一個棧容量,64位的數(shù)據(jù)類型占2個棧容量,且在方法執(zhí)行的任意時刻,操作數(shù)棧的深度都不會超過max_stacks中設(shè)置的最大值。
5、操作數(shù)棧并非采用訪問索引的方式來進(jìn)行數(shù)據(jù)訪問的,而是只能通過標(biāo)準(zhǔn)的入棧和出棧操作來完成一次數(shù)據(jù)訪問
6、如果被調(diào)用的方法帶有返回值的話,其返回值將會被壓入當(dāng)前棧幀的操作數(shù)棧中,并更新PC寄存器中下一條需要執(zhí)行的字節(jié)碼指令。
public void stackTest() {int a = 10;int b = 21;int c = a + b; } 0 bipush 10 // 10被壓入操作數(shù)堆棧。 2 istore_1 //從操作數(shù)堆棧中彈出一個數(shù) ,將這個數(shù)賦值給局部變量 a 這里istore_的索引之所以是一,而不是0,是因為局部變量表中,第一個放進(jìn)去的是this。 static方法中 沒有 this,那個時候索引才是從0開始。3 bipush 215 istore_2 // 同上6 iload_1 // iload_<n> <n > 必須是當(dāng)前幀的局部變量數(shù)組的索引。< n >處的局部變量必須包含一個int. < n >處的局部變量的值被壓入操作數(shù)堆棧。7 iload_28 iadd // 執(zhí)行 add 操作9 istore_3 // 將結(jié)果賦值到局部變量 索引為3的變量上。 10 return我想看完這個gif動圖 ,我想大家大概能夠明白操作數(shù)棧是一個什么樣的流程了吧,或者已經(jīng)明白了吧。如果沒有明白的話,可以留言評論哈。
2.7、動態(tài)鏈接
概述
動態(tài)鏈接(Dynamic Linking): 每個棧幀都保存了 一個 可以指向當(dāng)前方法所在類的 運行時常量池, 目的是: 當(dāng)前方法中如果需要調(diào)用其他方法的時候, 能夠從運行時常量池中找到對應(yīng)的符號引用, 然后將符號引用轉(zhuǎn)換為直接引用,然后就能直接調(diào)用對應(yīng)方法, 這就是動態(tài)鏈接。 比如:invokedynamic指令
👍
在Java源文件被編譯到字節(jié)碼文件中時,所有的變量和方法引用都作為符號引用(symbolic Reference)保存在class文件的常量池里。
小思考:為什么需要運行時常量池?
因為在不同的方法,都可能調(diào)用常量或者方法,所以只需要存儲一份即可,節(jié)省了空間。
常量池的作用:就是為了提供一些符號和常量,便于指令的識別
比如:描述一個方法調(diào)用了另外的其他方法時,就是通過常量池中指向方法的符號引用來表示的,那么動態(tài)鏈接的作用就是為了將這些符號引用轉(zhuǎn)換為調(diào)用方法的直接引用。
講這么這么多,沒有親眼見過,其實還是會對所謂的動態(tài)鏈接感到陌生的,因為我也是的,所以接下來👇 給大家舉了栗子和圖哦。
1、代碼部分
2、通過 javap -v 類名.class 進(jìn)行反編譯后
-
main 方法
-
描述一個方法調(diào)用了另外的其他方法時,就是通過常量池中指向方法的符號引用來表示的 。
-
注意圖中 調(diào)用test 方法中的那一行指令 invokevirtual #6 // Method test:()V
-
invokevirtual #6 :調(diào)用實例方法;基于類調(diào)度 。
-
那么#6是什么意思呢? 這就牽扯到了常量池啦。
-
我們接著來看一下 常量池(Constant pool)
-
#6 又接著指向了 #4.#33 但其實 # 6 后面的注釋已經(jīng)講出來了。// StackFrameTest.test:()V
-
#4 是 Class, StackFrameTest 實例。
-
#33 又接著執(zhí)行#15:#9 也就是后面的注解 // test:() V
-
test 說的是方法名 ()V 說的返回值是 void。
鏈接
靜態(tài)鏈接:
當(dāng)一個字節(jié)碼文件被裝載進(jìn)JVM內(nèi)部時,如果被調(diào)用的目標(biāo)方法在編譯期克制,且運行期保持不變時,這種情況下降調(diào)用方法的符號引用轉(zhuǎn)換為直接引用的過程稱之為靜態(tài)鏈接
動態(tài)鏈接:
如果被調(diào)用的方法在編譯期無法被確定下來,也就是說,只能夠在程序運行期將調(diào)用的方法的符號轉(zhuǎn)換為直接引用,由于這種引用轉(zhuǎn)換過程具備動態(tài)性,因此也被稱之為動態(tài)鏈接。
這個動態(tài)鏈接只從粗略的角度講了,里面其實還有一些內(nèi)容沒講,考慮到篇幅過長,有時間會再補(bǔ)一篇動態(tài)鏈接的文章。
2.8、方法返回地址
存放調(diào)用該方法的pc寄存器的值。當(dāng)一個方法開始執(zhí)行后,只有兩種方式可以退出這個方法:
-
正常完成出口:執(zhí)行引擎遇到任意一個方法返回的字節(jié)碼指令(return),會有返回值傳遞給上層的方法調(diào)用者,簡稱正常完成出口;究竟需要使用哪一個返回指令,還需要根據(jù)方法返回值的實際數(shù)據(jù)類型而定。
-
異常完成出口 :在方法執(zhí)行過程中遇到異常(Exception),并且這個異常沒有在方法內(nèi)進(jìn)行處理,也就是只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導(dǎo)致方法退出,簡稱異常完成出口。
無論通過哪種方式退出,在方法退出后都返回到該方法被調(diào)用的位置。方法正常退出時,調(diào)用者的pc計數(shù)器的值作為返回地址,即調(diào)用該方法的指令的下一條指令的地址。而通過異常退出的,返回地址是要通過異常表來確定,棧幀中一般不會保存這部分信息。
異常表:
方法執(zhí)行過程中,拋出異常時的異常處理,存儲在一個異常處理表,方便在發(fā)生異常的時候找到處理異常的代碼
本質(zhì)上,方法的退出就是當(dāng)前棧幀出棧的過程。此時,需要恢復(fù)上層方法的局部變量表、操作數(shù)棧、將返回值壓入調(diào)用者棧幀的操作數(shù)棧、設(shè)置PC寄存器值等,讓調(diào)用者方法繼續(xù)執(zhí)行下去。
正常完成出口和異常完成出口的區(qū)別在于:通過異常完成出口退出的不會給他的上層調(diào)用者產(chǎn)生任何的返回值。
2.9、一些附加信息
棧幀中還允許攜帶與Java虛擬機(jī)實現(xiàn)相關(guān)的一些附加信息。例如:對程序調(diào)試提供支持的信息。
課間休息會哈 接著看題啦
面試
面試提問:
1、這個棧內(nèi)存大小是設(shè)置的越大越好嗎????是的話,是為什么?不是的話,又是為什么?
- 不是,一定時間內(nèi)降低了OOM概率,但是會擠占其它的線程空間,因為整個空間是有限的。
2、垃圾回收是否涉及到虛擬機(jī)棧?
- 不會
3、方法中定義的局部變量是否線程安全?
- 具體問題具體分析。看到這一點你可能會產(chǎn)生一些疑惑,我也理解。
- 為什么會產(chǎn)生疑惑呢?我講過局部變量表是線程私有的,竟然都是私有的,肯定是線程安全的啊,但是這有一個前提的,如果這個局部變量在方法內(nèi)部產(chǎn)生,又在方法內(nèi)部消亡,生命周期是和棧楨相同的,那么可以肯定是它是線程安全的。但是如果這個方法是需要接收參數(shù),或者是需要返回值,那么這個時候就可以需要具體分析啦。
自言自語
這兩天河南發(fā)生了大暴雨,希望他們平安度過!!!
兄弟們,還是一起躺平吧。內(nèi)卷太累辣吧。。。
👩?💻->👨?💻🛌🛌
總結(jié)
以上是生活随笔為你收集整理的JVM-虚拟机栈详解 附面试高频题 (手画多图)!!!深入浅出,绝对值得收藏哈!!!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 双亲委派机制 详解(手画详图)面试高频
- 下一篇: 一篇文章带你快速理解JVM运行时数据区