探究 Java 虛擬機(jī)棧

前言

Java 虛擬機(jī)的內(nèi)存模型分為兩部分:一部分是線程共享的,包括 Java 堆和方法區(qū);另一部分是線程私有的栓撞,包括虛擬機(jī)棧和本地方法棧,以及程序計(jì)數(shù)器這一小部分內(nèi)存碗硬。今天我就 Java 虛擬機(jī)棧做一些比較淺的探究瓤湘。

熟悉 Java 的同學(xué)應(yīng)該都知道了,JVM 是基于棧的恩尾。但是這個(gè)“棾谒担” 具體指的是什么?難道就是虛擬機(jī)棧翰意?想要回答這個(gè)問題我們先要從虛擬機(jī)棧的結(jié)構(gòu)談起木人。

虛擬機(jī)棧

何為虛擬機(jī)棧

虛擬機(jī)棧的棧元素是棧幀信柿,當(dāng)有一個(gè)方法被調(diào)用時(shí),代表這個(gè)方法的棧幀入棧醒第;當(dāng)這個(gè)方法返回時(shí)角塑,其棧幀出棧。因此淘讥,虛擬機(jī)棧中棧幀的入棧順序就是方法調(diào)用順序圃伶。什么是棧幀呢?棧幀可以理解為一個(gè)方法的運(yùn)行空間蒲列。它主要由兩部分構(gòu)成窒朋,一部分是局部變量表,方法中定義的局部變量以及方法的參數(shù)就存放在這張表中蝗岖;另一部分是操作數(shù)棧侥猩,用來存放操作數(shù)。我們知道抵赢,Java 程序編譯之后就變成了一條條字節(jié)碼指令欺劳,其形式類似匯編,但和匯編有不同之處:匯編指令的操作數(shù)存放在數(shù)據(jù)段和寄存器中铅鲤,可通過存儲(chǔ)器或寄存器尋址找到需要的操作數(shù)划提;而 Java 字節(jié)碼指令的操作數(shù)存放在操作數(shù)棧中,當(dāng)執(zhí)行某條帶 n 個(gè)操作數(shù)的指令時(shí)邢享,就從棧頂取 n 個(gè)操作數(shù)鹏往,然后把指令的計(jì)算結(jié)果(如果有的話)入棧。因此骇塘,當(dāng)我們說 JVM 執(zhí)行引擎是基于棧的時(shí)候伊履,其中的“棧”指的就是操作數(shù)棧款违。舉個(gè)簡(jiǎn)單的例子對(duì)比下匯編指令和 Java 字節(jié)碼指令的執(zhí)行過程唐瀑,比如計(jì)算 1 + 2,在匯編指令是這樣的:

1

2

mov ax, 1;把 1放入寄存器 ax

add ax, 2;用 ax 的內(nèi)容和 2相加后存入 ax

而 JVM 的字節(jié)碼指令是這樣的:

1

2

3

iconst_1 //把整數(shù) 1 壓入操作數(shù)棧

iconst_2 //把整數(shù) 2 壓入操作數(shù)棧

iadd //棧頂?shù)膬蓚€(gè)數(shù)相加后出棧插爹,結(jié)果入棧

由于操作數(shù)棧是內(nèi)存空間哄辣,所以字節(jié)碼指令不必?fù)?dān)心不同機(jī)器上寄存器以及機(jī)器指令的差別,從而做到了平臺(tái)無關(guān)递惋。

注意柔滔,局部變量表中的變量不可直接使用溢陪,如需使用必須通過相關(guān)指令將其加載至操作數(shù)棧中作為操作數(shù)使用萍虽。比如有一個(gè)方法 void foo(),其中的代碼為:int a = 1 + 2; int b = a + 3;形真,編譯為字節(jié)碼指令就是這樣的:

1

2

3

4

5

6

7

8

9

iconst_1 //把整數(shù) 1 壓入操作數(shù)棧

iconst_2 //把整數(shù) 2 壓入操作數(shù)棧

iadd //棧頂?shù)膬蓚€(gè)數(shù)出棧后相加杉编,結(jié)果入棧超全;實(shí)際上前三步會(huì)被編譯器優(yōu)化為:iconst_3

istore_1 //把棧頂?shù)膬?nèi)容放入局部變量表中索引為 1 的 slot 中,也就是 a 對(duì)應(yīng)的空間中

iload_1 // 把局部變量表索引為 1 的 slot 中存放的變量值(3)加載至操作數(shù)棧

iconst_3

iadd //棧頂?shù)膬蓚€(gè)數(shù)出棧后相加邓馒,結(jié)果入棧

istore_2 // 把棧頂?shù)膬?nèi)容放入局部變量表中索引為 2 的 slot 中嘶朱,也就是 b 對(duì)應(yīng)的空間中

return// 方法返回指令,回到調(diào)用點(diǎn)

需要說明的是光酣,局部變量表以及操作數(shù)棧的容量的最大值在編譯時(shí)就已經(jīng)確定了疏遏,運(yùn)行時(shí)不會(huì)改變。并且局部變量表的空間是可以復(fù)用的救军,例如财异,當(dāng)指令的位置超出了局部變量表中某個(gè)變量 a 的作用域時(shí),如果有新的局部變量 b 要被定義唱遭,b 就會(huì)覆蓋 a 在局部變量表的空間戳寸。

盜用別人的圖以讓大家對(duì)虛擬機(jī)棧有個(gè)直觀的認(rèn)識(shí)(其中小字體 Stack 指的的是虛擬機(jī)棧,F(xiàn)rame 是棧幀拷泽,Local variables 是局部變量表疫鹊,Operand Stack 是操作數(shù)棧):

虛擬機(jī)棧

由虛擬機(jī)棧引出的問題

看完上面的代碼大家可能會(huì)有幾點(diǎn)疑惑:什么是 slot?那些指令是什么意思司致?為什么 a 對(duì)應(yīng)的 slot 的索引值不是從零開始的拆吆,它明明是第一個(gè)定義的變量啊脂矫?

對(duì)于這些問題我們一個(gè)個(gè)來解決锈拨。

什么是 slot

首先什么是 slot?slot 是局部變量表中的空間單位羹唠,虛擬機(jī)規(guī)范中有規(guī)定奕枢,對(duì)于 32 位之內(nèi)的數(shù)據(jù),用一個(gè) slot 來存放佩微,如 int缝彬,short,float 等哺眯;對(duì)于 64 位的數(shù)據(jù)用連續(xù)的兩個(gè) slot 來存放谷浅,如 long,double 等奶卓。引用類型的變量 JVM 并沒有規(guī)定其長(zhǎng)度一疯,它可能是 32 位,也有可能是 64 位的夺姑,所以既有可能占一個(gè) slot墩邀,也有可能占兩個(gè) slot。

JVM 字節(jié)碼指令

第二個(gè)問題盏浙,那些指令是什么意思眉睹?

指令格式

首先我們要理解 Java 指令的格式荔茬,Java 的指令以字節(jié)為單位,也就是一個(gè)字節(jié)代表一條指令竹海。比如 iconst_1 就是一條指令慕蔚,它占一個(gè)字節(jié),那么自然 Java 指令不會(huì)超過 256 條斋配。實(shí)際上 Java 指令目前定義了 200 多條孔飒。指令雖然是一個(gè)字節(jié),但是它也可以帶自己的操作數(shù)艰争。JVM 中有這樣一條指令 putstatic十偶,其作用是給特定的的靜態(tài)字段賦值。但是給哪個(gè)字段賦值呢园细??jī)H僅通過這條指令并不能說明惦积,那么只有通過操作數(shù)來指定了。緊跟在 putstatic 后面的兩個(gè)字節(jié)就是它的操作數(shù)猛频,這個(gè)操作數(shù)是一個(gè)索引值狮崩,指向運(yùn)行時(shí)常量池中該靜態(tài)字段對(duì)應(yīng)的符號(hào)引用。由于符號(hào)引用包含了該字段的基本信息鹿寻,如所屬類睦柴、簡(jiǎn)單名稱以及描述符,因此 putstatic 指令就知道是給哪個(gè)類的哪個(gè)字段賦值了毡熏。

指令的操作數(shù)分兩種:一種是嵌入在指令中的坦敌,通常是指令字節(jié)后面的若干個(gè)字節(jié);另一種是存放在操作數(shù)棧中的痢法。為了區(qū)別狱窘,我們把前者叫做嵌入式操作數(shù),把后者叫做棧內(nèi)操作數(shù)财搁。這兩者的區(qū)別是:嵌入式操作數(shù)是在編譯時(shí)就已經(jīng)確定的蘸炸,運(yùn)行時(shí)不會(huì)改變,它和指令一樣存放于類文件方法表的 Code 屬性中尖奔;而操作數(shù)是運(yùn)行時(shí)確定的搭儒,即程序在執(zhí)行過程中動(dòng)態(tài)生成的。拿 putstatic 指令來說提茁,它有一個(gè)嵌入式操作數(shù)淹禾,該操作數(shù)是一個(gè)索引值(前面已經(jīng)提到),它由兩個(gè)字節(jié)組成茴扁,緊跟在 putstatic 對(duì)應(yīng)的字節(jié)之后铃岔;同時(shí)它還有一個(gè)棧內(nèi)操作數(shù),位于操作數(shù)棧的棧頂丹弱,這個(gè)操作數(shù)就是要賦給靜態(tài)字段的值德撬,其對(duì)應(yīng)的字節(jié)數(shù)根據(jù)靜態(tài)字段的類型決定铲咨。如果靜態(tài)字段的類型是 short躲胳、int蜓洪、boolean、char 或者 byte坯苹,那么這個(gè)操作數(shù)就必須是 int 類型隆檀,即由棧頂?shù)?4 個(gè)字節(jié)組成;如果是 float粹湃、double 或者 long 類型恐仑,那么操作數(shù)就是相應(yīng)的類型,即由棧頂?shù)?4 個(gè)为鳄、8 個(gè) 或者 8 個(gè) 字節(jié)組成裳仆;如果靜態(tài)字段是引用類型,那么這個(gè)操作數(shù)的類型也必須是引用類型孤钦,即由棧頂?shù)?8 個(gè)字節(jié)組成歧斟。

再舉一個(gè)例子。iconst_<i> 代表了一個(gè)指令族偏形,它的意思是把整數(shù) i 放入操作數(shù)棧中静袖,i 的范圍是(m1, 0, 1, 2, 3, 4, 5),其中 m1 代表的是 -1俊扭。注意队橙,這里的 i 并不是指令的操作數(shù)(即非嵌入式操作數(shù),也非棧內(nèi)操作數(shù))萨惑,如 iconst_1捐康、iconst_2 和 iconst_3 都是由一個(gè)字節(jié)組成的字節(jié)碼指令。我們可以把 i 可以看作是指令的 “隱含操作數(shù)”庸蔼,即指令本身就蘊(yùn)含了操作數(shù)吹由。如果整數(shù) i 超過 [-1, 5] 這個(gè)范圍,就不能用 iconst_<i> 表示了朱嘴,因?yàn)閮H一個(gè)字節(jié)的字節(jié)碼指令不可能蘊(yùn)含所有的整數(shù)倾鲫。此時(shí)就需要 bipush 這條指令了,這條指令有一個(gè)嵌入式操作數(shù)萍嬉,由一個(gè)字節(jié)組成乌昔,用來表示要放入棧頂?shù)哪莻€(gè)整數(shù),該整數(shù)放入棧頂時(shí)通過擴(kuò)展符號(hào)位變?yōu)?32 位的整型壤追。但是一個(gè)字節(jié)也表示不了所有的整數(shù)磕道,如果整數(shù)值超過一個(gè)字節(jié)所能表示的范圍,就只能通過 ldc 這條指令了行冰,這條指令帶有一個(gè)字節(jié)的嵌入式操作數(shù)溺蕉,它代表的是一個(gè)指向運(yùn)行時(shí)常量池中 Constant_Integer_info 類型常量的索引伶丐,通過索引的方式引用運(yùn)行時(shí)常量池中的整數(shù),再大的整數(shù)也不怕了疯特。

閱讀指令文檔

授之以魚不如授之以漁哗魂,在這里不可能將所有的指令都講解一番,因此我和大家分享一下如何閱讀 oracle 官網(wǎng)關(guān)于字節(jié)碼指令的文檔吧漓雅。文檔的地址是:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html

我們拿 astore 指令來說: 關(guān)于它的文檔描述如下:

說明和翻譯:

第一行的粗體字是指令的名稱录别;

Operation 是指令的功能:把引用存入本地變量中;

Format 是指令的格式:它的第一個(gè)字節(jié)是指令邻吞,名稱為 astore组题,第二個(gè)字節(jié)是指令的嵌入式操作數(shù),名稱為 index抱冷;Forms 指的是指令的十進(jìn)制(十六進(jìn)制)碼崔列,astore 的十進(jìn)制(十六進(jìn)制)碼是 58(0x3a);

Operation Stack 是指令執(zhí)行前后的操作數(shù)棧的狀態(tài):第一行代表的是指令執(zhí)行前操作數(shù)的狀態(tài)旺遮,第二行是指令執(zhí)行后操作數(shù)棧的狀態(tài)赵讯,箭頭是棧頂方向。astore 執(zhí)行前棧頂是對(duì)象引用 objectRef趣效,它是 astore 的棧內(nèi)操作數(shù)瘦癌,執(zhí)行后 objectRef 被彈出并存入局部變量表中;

Description 是對(duì)這條指令的描述:index 是無符號(hào)字節(jié)跷敬,這個(gè) index 必須指向當(dāng)前棧幀的局部變量表的某個(gè)位置讯私。操作數(shù)棧的棧頂?shù)哪莻€(gè)引用值必須是 returnAddress(方法返回地址)或者是 reference (對(duì)象引用)。這個(gè)引用會(huì)被彈出西傀,其值會(huì)被存入局部變量表中索引為 index 的 slot 中斤寇;

Notes 是注意事項(xiàng):實(shí)現(xiàn) Java 中的 finally 子句時(shí),astore 指令使用的操作數(shù)類型是一個(gè) returnAddress拥褂,與 astore 對(duì)應(yīng)的 aload 指令(將局部變量表的的引用值壓棧)不能將類型為 returnAddress 類型的值加載到操作數(shù)棧娘锁,而只能是 reference 類型。aload 和 astore 這種不對(duì)稱的設(shè)計(jì)是有意而為之的饺鹃。astore 指令可以和 wide 指令配合使用以用無符號(hào)雙字節(jié)類型的索引來獲取局部變量表中的變量莫秆。

局部變量表的第一個(gè)變量

從 Java 語(yǔ)言的層面講,靜態(tài)方法和實(shí)例方法的本質(zhì)區(qū)別在于是否是對(duì)象所共享的悔详。而從 JVM 的角度來看镊屎,方法(無論靜態(tài)方法還是實(shí)例方法)其實(shí)都是對(duì)象共享的,實(shí)例變量才是對(duì)象私有的茄螃。對(duì) JVM 而言缝驳,靜態(tài)方法和實(shí)例方法的本質(zhì)區(qū)別在于是否需要和具體對(duì)象關(guān)聯(lián):靜態(tài)方法可以通過類名來調(diào)用,它不需要和具體對(duì)象關(guān)聯(lián);而實(shí)例方法必須通過對(duì)象來進(jìn)行調(diào)用用狱,它需要和具體對(duì)象關(guān)聯(lián)运怖。那么,實(shí)例方法和具體對(duì)象是如何產(chǎn)生關(guān)聯(lián)的呢夏伊?其實(shí)很簡(jiǎn)單摇展,編譯器在編譯時(shí)會(huì)將方法接收者作為一個(gè)隱含參數(shù)傳入該實(shí)例方法,這個(gè)參數(shù)在方法中有一個(gè)很熟悉的名字署海,叫做 “this”吗购。之所以實(shí)例方法可以訪問該類的實(shí)例變量和其它實(shí)例方法医男,正是因?yàn)樗?“this” 這個(gè)隱含參數(shù)砸狞。舉個(gè)例子,類 A 中的某個(gè)方法 b 需要訪問實(shí)例變量 x镀梭,由于實(shí)例變量是對(duì)象私有的刀森,如果 b 是靜態(tài)方法,由于它沒有具體對(duì)象的引用报账,它并不知道該訪問哪個(gè)對(duì)象的實(shí)例變量 x研底;如果 b 是實(shí)例方法,通過隱含參數(shù) this 就能確定要訪問的實(shí)例變量是 this.x透罢。那么榜晦,為什么靜態(tài)方法也不能調(diào)用該類的實(shí)例方法呢?本質(zhì)原因也是沒有 this 引用羽圃。因?yàn)檎{(diào)用實(shí)例方法的前提是要傳入一個(gè)隱含參數(shù)乾胶,實(shí)例方法本來就有這個(gè)引用,所以能夠把它作為隱含參數(shù)傳入另一個(gè)實(shí)例方法朽寞;靜態(tài)方法沒有 this 引用识窿,無法給實(shí)例方法提供指向方法接收者的隱含參數(shù),因此不能調(diào)用實(shí)例方法脑融。

如果看懂了上面說的那些喻频,第三個(gè)問題也就迎刃而解了。因?yàn)槲覀兌x的方法是 void foo()肘迎,它是實(shí)例方法甥温,因此會(huì)有一個(gè)指向具體對(duì)象的隱含參數(shù) this,this 就存放在局部變量表的第一個(gè)位置妓布,即存放在索引為 0 的 slot 中姻蚓,又由于它的作用域從方法開始一直到方法結(jié)束,因此它在局部變量表中的位置不會(huì)被其他變量覆蓋秋茫,從而使得我們?cè)诜椒ㄖ卸x的變量只能放在局部變量表后面的位置中史简。需要注意的是,如果方法有參數(shù)(非隱含參數(shù)),那么參數(shù)會(huì)按順序緊接著 this 存放在局部變量表中圆兵,由于參數(shù)作用域也是整個(gè)方法體跺讯,所以方法中定義的局部變量就只能放在參數(shù)后面了⊙撑總的來說局部變量表中變量的存放順序?yàn)椋?this(如果是實(shí)例方法)=> 參數(shù)(如果有的話)=> 定義的局部變量(如果有的話)刀脏。

感謝閱讀

關(guān)于虛擬機(jī)棧就講這么多了,Java 虛擬機(jī)是一個(gè)完整的知識(shí)體系超凳,僅僅了解虛擬機(jī)棧是不夠的愈污,這里沒有細(xì)講的關(guān)于虛擬機(jī)的其它知識(shí),如內(nèi)存模型轮傍、運(yùn)行時(shí)常量池暂雹、類加載模型等,還需讀者自己學(xué)習(xí)掌握创夜。這篇文章權(quán)起激發(fā)大家的學(xué)習(xí) JVM 興趣的作用杭跪,同時(shí)也作為個(gè)人的學(xué)習(xí)記錄和知識(shí)總結(jié)。之后可能還會(huì)寫些 JVM 其它方面的總結(jié)性文章和大家分享驰吓。由于個(gè)人水平和理解有限涧尿,如果有不對(duì)的地方還請(qǐng)大家不吝賜教,感謝閱讀檬贰!

參考書籍 :

《深入理解 Java 虛擬機(jī)》周志明 著

Java 虛擬機(jī)規(guī)范 :?The Java? Virtual Machine Specification

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末姑廉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子翁涤,更是在濱河造成了極大的恐慌桥言,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迷雪,死亡現(xiàn)場(chǎng)離奇詭異限书,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)章咧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門倦西,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人赁严,你說我怎么就攤上這事扰柠。” “怎么了疼约?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵卤档,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我程剥,道長(zhǎng)劝枣,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮舔腾,結(jié)果婚禮上溪胶,老公的妹妹穿的比我還像新娘。我一直安慰自己稳诚,他們只是感情好哗脖,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著扳还,像睡著了一般才避。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上氨距,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天桑逝,我揣著相機(jī)與錄音,去河邊找鬼衔蹲。 笑死肢娘,一個(gè)胖子當(dāng)著我的面吹牛呈础,可吹牛的內(nèi)容都是我干的舆驶。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼而钞,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼沙廉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起臼节,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤撬陵,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后网缝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體巨税,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年粉臊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了草添。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡扼仲,死狀恐怖远寸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情屠凶,我是刑警寧澤驰后,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站矗愧,受9級(jí)特大地震影響灶芝,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一夜涕、第九天 我趴在偏房一處隱蔽的房頂上張望颤专。 院中可真熱鬧,春花似錦钠乏、人聲如沸栖秕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)簇捍。三九已至,卻和暖如春俏拱,著一層夾襖步出監(jiān)牢的瞬間暑塑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工锅必, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留事格,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓搞隐,卻偏偏與公主長(zhǎng)得像驹愚,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子劣纲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容