探究Java虛擬機(jī)棧-創(chuàng)新互聯(lián)

前言

專業(yè)成都網(wǎng)站建設(shè)公司,做排名好的好網(wǎng)站,排在同行前面,為您帶來客戶和效益!成都創(chuàng)新互聯(lián)公司為您提供成都網(wǎng)站建設(shè),五站合一網(wǎng)站設(shè)計(jì)制作,服務(wù)好的網(wǎng)站設(shè)計(jì)公司,網(wǎng)站設(shè)計(jì)、成都網(wǎng)站設(shè)計(jì)負(fù)責(zé)任的成都網(wǎng)站制作公司!

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

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

虛擬機(jī)棧

何為虛擬機(jī)棧
虛擬機(jī)棧的棧元素是棧幀,當(dāng)有一個方法被調(diào)用時,代表這個方法的棧幀入棧;當(dāng)這個方法返回時,其棧幀出棧。因此,虛擬機(jī)棧中棧幀的入棧順序就是方法調(diào)用順序。什么是棧幀呢?棧幀可以理解為一個方法的運(yùn)行空間。它主要由兩部分構(gòu)成,一部分是局部變量表,方法中定義的局部變量以及方法的參數(shù)就存放在這張表中;另一部分是操作數(shù)棧,用來存放操作數(shù)。我們知道,Java 程序編譯之后就變成了一條條字節(jié)碼指令,其形式類似匯編,但和匯編有不同之處:匯編指令的操作數(shù)存放在數(shù)據(jù)段和寄存器中,可通過存儲器或寄存器尋址找到需要的操作數(shù);而 Java 字節(jié)碼指令的操作數(shù)存放在操作數(shù)棧中,當(dāng)執(zhí)行某條帶 n 個操作數(shù)的指令時,就從棧頂取 n 個操作數(shù),然后把指令的計(jì)算結(jié)果(如果有的話)入棧。因此,當(dāng)我們說 JVM 執(zhí)行引擎是基于棧的時候,其中的“?!敝傅木褪遣僮鲾?shù)棧。舉個簡單的例子對比下匯編指令和 Java 字節(jié)碼指令的執(zhí)行過程,比如計(jì)算 1 + 2,在匯編指令是這樣的:

mov ax, 1 ;把 1 放入寄存器 ax
add ax, 2 ;用 ax 的內(nèi)容和 2 相加后存入 ax
而 JVM 的字節(jié)碼指令是這樣的:

iconst_1 //把整數(shù) 1 壓入操作數(shù)棧
iconst_2 //把整數(shù) 2 壓入操作數(shù)棧
iadd //棧頂?shù)膬蓚€數(shù)相加后出棧,結(jié)果入棧
由于操作數(shù)棧是內(nèi)存空間,所以字節(jié)碼指令不必?fù)?dān)心不同機(jī)器上寄存器以及機(jī)器指令的差別,從而做到了平臺無關(guān)。

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

iconst_1 //把整數(shù) 1 壓入操作數(shù)棧
iconst_2 //把整數(shù) 2 壓入操作數(shù)棧
iadd //棧頂?shù)膬蓚€數(shù)出棧后相加,結(jié)果入棧;實(shí)際上前三步會被編譯器優(yōu)化為:iconst_3
istore_1 //把棧頂?shù)膬?nèi)容放入局部變量表中索引為 1 的 slot 中,也就是 a 對應(yīng)的空間中
iload_1 // 把局部變量表索引為 1 的 slot 中存放的變量值(3)加載至操作數(shù)棧
iconst_3
iadd //棧頂?shù)膬蓚€數(shù)出棧后相加,結(jié)果入棧
istore_2 // 把棧頂?shù)膬?nèi)容放入局部變量表中索引為 2 的 slot 中,也就是 b 對應(yīng)的空間中
return // 方法返回指令,回到調(diào)用點(diǎn)
需要說明的是,局部變量表以及操作數(shù)棧的容量的大值在編譯時就已經(jīng)確定了,運(yùn)行時不會改變。并且局部變量表的空間是可以復(fù)用的,例如,當(dāng)指令的位置超出了局部變量表中某個變量 a 的作用域時,如果有新的局部變量 b 要被定義,b 就會覆蓋 a 在局部變量表的空間。

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

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

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

對于這些問題我們一個個來解決。

什么是 slot
首先什么是 slot?slot 是局部變量表中的空間單位,虛擬機(jī)規(guī)范中有規(guī)定,對于 32 位之內(nèi)的數(shù)據(jù),用一個 slot 來存放,如 int,short,float 等;對于 64 位的數(shù)據(jù)用連續(xù)的兩個 slot 來存放,如 long,double 等。引用類型的變量 JVM 并沒有規(guī)定其長度,它可能是 32 位,也有可能是 64 位的,所以既有可能占一個 slot,也有可能占兩個 slot。

JVM 字節(jié)碼指令
第二個問題,那些指令是什么意思?

指令格式

首先我們要理解 Java 指令的格式,Java 的指令以字節(jié)為單位,也就是一個字節(jié)代表一條指令。比如 iconst_1 就是一條指令,它占一個字節(jié),那么自然 Java 指令不會超過 256 條。實(shí)際上 Java 指令目前定義了 200 多條。指令雖然是一個字節(jié),但是它也可以帶自己的操作數(shù)。JVM 中有這樣一條指令 putstatic,其作用是給特定的的靜態(tài)字段賦值。但是給哪個字段賦值呢?僅僅通過這條指令并不能說明,那么只有通過操作數(shù)來指定了。緊跟在 putstatic 后面的兩個字節(jié)就是它的操作數(shù),這個操作數(shù)是一個索引值,指向運(yùn)行時常量池中該靜態(tài)字段對應(yīng)的符號引用。由于符號引用包含了該字段的基本信息,如所屬類、簡單名稱以及描述符,因此 putstatic 指令就知道是給哪個類的哪個字段賦值了。

指令的操作數(shù)分兩種:一種是嵌入在指令中的,通常是指令字節(jié)后面的若干個字節(jié);另一種是存放在操作數(shù)棧中的。為了區(qū)別,我們把前者叫做嵌入式操作數(shù),把后者叫做棧內(nèi)操作數(shù)。這兩者的區(qū)別是:嵌入式操作數(shù)是在編譯時就已經(jīng)確定的,運(yùn)行時不會改變,它和指令一樣存放于類文件方法表的 Code 屬性中;而操作數(shù)是運(yùn)行時確定的,即程序在執(zhí)行過程中動態(tài)生成的。拿 putstatic 指令來說,它有一個嵌入式操作數(shù),該操作數(shù)是一個索引值(前面已經(jīng)提到),它由兩個字節(jié)組成,緊跟在 putstatic 對應(yīng)的字節(jié)之后;同時它還有一個棧內(nèi)操作數(shù),位于操作數(shù)棧的棧頂,這個操作數(shù)就是要賦給靜態(tài)字段的值,其對應(yīng)的字節(jié)數(shù)根據(jù)靜態(tài)字段的類型決定。如果靜態(tài)字段的類型是 short、int、boolean、char 或者 byte,那么這個操作數(shù)就必須是 int 類型,即由棧頂?shù)?4 個字節(jié)組成;如果是 float、double 或者 long 類型,那么操作數(shù)就是相應(yīng)的類型,即由棧頂?shù)?4 個、8 個 或者 8 個 字節(jié)組成;如果靜態(tài)字段是引用類型,那么這個操作數(shù)的類型也必須是引用類型,即由棧頂?shù)?8 個字節(jié)組成。

再舉一個例子。iconst_<i> 代表了一個指令族,它的意思是把整數(shù) i 放入操作數(shù)棧中,i 的范圍是(m1, 0, 1, 2, 3, 4, 5),其中 m1 代表的是 -1。注意,這里的 i 并不是指令的操作數(shù)(即非嵌入式操作數(shù),也非棧內(nèi)操作數(shù)),如 iconst_1、iconst_2 和 iconst3 都是由一個字節(jié)組成的字節(jié)碼指令。我們可以把 i 可以看作是指令的 “隱含操作數(shù)”,即指令本身就蘊(yùn)含了操作數(shù)。如果整數(shù) i 超過 [-1, 5] 這個范圍,就不能用 iconst<i> 表示了,因?yàn)閮H一個字節(jié)的字節(jié)碼指令不可能蘊(yùn)含所有的整數(shù)。此時就需要 bipush 這條指令了,這條指令有一個嵌入式操作數(shù),由一個字節(jié)組成,用來表示要放入棧頂?shù)哪莻€整數(shù),該整數(shù)放入棧頂時通過擴(kuò)展符號位變?yōu)?32 位的整型。但是一個字節(jié)也表示不了所有的整數(shù),如果整數(shù)值超過一個字節(jié)所能表示的范圍,就只能通過 ldc 這條指令了,這條指令帶有一個字節(jié)的嵌入式操作數(shù),它代表的是一個指向運(yùn)行時常量池中 Constant_Integer_info 類型常量的索引,通過索引的方式引用運(yùn)行時常量池中的整數(shù),再大的整數(shù)也不怕了。

閱讀指令文檔

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

我們拿 astore 指令來說: 關(guān)于它的文檔描述如下:
探究Java虛擬機(jī)棧

astore 指令
說明和翻譯:

第一行的粗體字是指令的名稱;
Operation 是指令的功能:把引用存入本地變量中;
Format 是指令的格式:它的第一個字節(jié)是指令,名稱為 astore,第二個字節(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í)行前棧頂是對象引用 objectRef,它是 astore 的棧內(nèi)操作數(shù),執(zhí)行后 objectRef 被彈出并存入局部變量表中;
Description 是對這條指令的描述:index 是無符號字節(jié),這個 index 必須指向當(dāng)前棧幀的局部變量表的某個位置。操作數(shù)棧的棧頂?shù)哪莻€引用值必須是 returnAddress(方法返回地址)或者是 reference (對象引用)。這個引用會被彈出,其值會被存入局部變量表中索引為 index 的 slot 中;
Notes 是注意事項(xiàng):實(shí)現(xiàn) Java 中的 finally 子句時,astore 指令使用的操作數(shù)類型是一個 returnAddress,與 astore 對應(yīng)的 aload 指令(將局部變量表的的引用值壓棧)不能將類型為 returnAddress 類型的值加載到操作數(shù)棧,而只能是 reference 類型。aload 和 astore 這種不對稱的設(shè)計(jì)是有意而為之的。astore 指令可以和 wide 指令配合使用以用無符號雙字節(jié)類型的索引來獲取局部變量表中的變量。

局部變量表的第一個變量
從 Java 語言的層面講,靜態(tài)方法和實(shí)例方法的本質(zhì)區(qū)別在于是否是對象所共享的。而從 JVM 的角度來看,方法(無論靜態(tài)方法還是實(shí)例方法)其實(shí)都是對象共享的,實(shí)例變量才是對象私有的。對 JVM 而言,靜態(tài)方法和實(shí)例方法的本質(zhì)區(qū)別在于是否需要和具體對象關(guān)聯(lián):靜態(tài)方法可以通過類名來調(diào)用,它不需要和具體對象關(guān)聯(lián);而實(shí)例方法必須通過對象來進(jìn)行調(diào)用,它需要和具體對象關(guān)聯(lián)。那么,實(shí)例方法和具體對象是如何產(chǎn)生關(guān)聯(lián)的呢?其實(shí)很簡單,編譯器在編譯時會將方法接收者作為一個隱含參數(shù)傳入該實(shí)例方法,這個參數(shù)在方法中有一個很熟悉的名字,叫做 “this”。之所以實(shí)例方法可以訪問該類的實(shí)例變量和其它實(shí)例方法,正是因?yàn)樗?“this” 這個隱含參數(shù)。舉個例子,類 A 中的某個方法 b 需要訪問實(shí)例變量 x,由于實(shí)例變量是對象私有的,如果 b 是靜態(tài)方法,由于它沒有具體對象的引用,它并不知道該訪問哪個對象的實(shí)例變量 x;如果 b 是實(shí)例方法,通過隱含參數(shù) this 就能確定要訪問的實(shí)例變量是 this.x。那么,為什么靜態(tài)方法也不能調(diào)用該類的實(shí)例方法呢?本質(zhì)原因也是沒有 this 引用。因?yàn)檎{(diào)用實(shí)例方法的前提是要傳入一個隱含參數(shù),實(shí)例方法本來就有這個引用,所以能夠把它作為隱含參數(shù)傳入另一個實(shí)例方法;靜態(tài)方法沒有 this 引用,無法給實(shí)例方法提供指向方法接收者的隱含參數(shù),因此不能調(diào)用實(shí)例方法。

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

感謝閱讀

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

另外有需要云服務(wù)器可以了解下創(chuàng)新互聯(lián)scvps.cn,海內(nèi)外云服務(wù)器15元起步,三天無理由+7*72小時售后在線,公司持有idc許可證,提供“云服務(wù)器、裸金屬服務(wù)器、高防服務(wù)器、香港服務(wù)器、美國服務(wù)器、虛擬主機(jī)、免備案服務(wù)器”等云主機(jī)租用服務(wù)以及企業(yè)上云的綜合解決方案,具有“安全穩(wěn)定、簡單易用、服務(wù)可用性高、性價比高”等特點(diǎn)與優(yōu)勢,專為企業(yè)上云打造定制,能夠滿足用戶豐富、多元化的應(yīng)用場景需求。

本文題目:探究Java虛擬機(jī)棧-創(chuàng)新互聯(lián)
網(wǎng)站路徑:http://muchs.cn/article36/iccpg.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站內(nèi)鏈、網(wǎng)站排名、服務(wù)器托管、企業(yè)網(wǎng)站制作、動態(tài)網(wǎng)站、定制開發(fā)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

微信小程序開發(fā)