探究Java虛擬機(jī)?!狫ava進(jìn)階 二維碼
發(fā)表時(shí)間:2018-11-01 00:00 前言 Java 虛擬機(jī)的內(nèi)存模型分為兩部分:一部分是線程共享的,包括 Java 堆和方法區(qū);另一部分是線程私有的,包括虛擬機(jī)棧和本地方法棧,以及程序計(jì)數(shù)器這一小部分內(nèi)存。今天我就 Java 虛擬機(jī)棧做一些比較淺的探究。 熟悉 Java 的同學(xué)應(yīng)該都知道了,JVM 是基于棧的。但是這個(gè)“棧” 具體指的是什么?難道就是虛擬機(jī)棧?想要回答這個(gè)問(wèn)題我們先要從虛擬機(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ù)棧,用來(lái)存放操作數(shù)。我們知道,Java 程序編譯之后就變成了一條條字節(jié)碼指令,其形式類似匯編,但和匯編有不同之處:匯編指令的操作數(shù)存放在數(shù)據(jù)段和寄存器中,可通過(guò)存儲(chǔ)器或寄存器尋址找到需要的操作數(shù);而 Java 字節(jié)碼指令的操作數(shù)存放在操作數(shù)棧中,當(dāng)執(zhí)行某條帶 n 個(gè)操作數(shù)的指令時(shí),就從棧頂取 n 個(gè)操作數(shù),然后把指令的計(jì)算結(jié)果(如果有的話)入棧。因此,當(dāng)我們說(shuō) JVM 執(zhí)行引擎是基于棧的時(shí)候,其中的“?!敝傅木褪遣僮鲾?shù)棧。舉個(gè)簡(jiǎn)單的例子對(duì)比下匯編指令和 Java 字節(jié)碼指令的執(zhí)行過(guò)程,比如計(jì)算 1 + 2,在匯編指令是這樣的: mov ax,1;把1放入寄存器 axadd ax,2;用 ax 的內(nèi)容和2相加后存入 ax 而 JVM 的字節(jié)碼指令是這樣的: 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)無(wú)關(guān)。 注意,局部變量表中的變量不可直接使用,如需使用必須通過(guò)相關(guān)指令將其加載操作數(shù)棧中作為操作數(shù)使用。比如有一個(gè)方法 void foo(),其中的代碼為:int a = 1 + 2; int b = a + 3;,編譯為字節(jié)碼指令就是這樣的: iconst_1 //把整數(shù) 1 壓入操作數(shù)棧iconst_2 //把整數(shù) 2 壓入操作數(shù)棧iadd //棧頂?shù)膬蓚€(gè)數(shù)出棧后相加,結(jié)果入棧;實(shí)際上前三步會(huì)被編譯器優(yōu)化為:iconst_3istore_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) 需要說(shuō)明的是,局部變量表以及操作數(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ī)棧引出的問(wèn)題 看完上面的代碼大家可能會(huì)有幾點(diǎn)疑惑:什么是 slot?那些指令是什么意思?為什么 a 對(duì)應(yīng)的 slot 的索引值不是從零開始的,它明明是靠前個(gè)定義的變量??? 對(duì)于這些問(wèn)題我們一個(gè)個(gè)來(lái)解決。 什么是 slot 首先什么是 slot?slot 是局部變量表中的空間單位,虛擬機(jī)規(guī)范中有規(guī)定,對(duì)于 32 位之內(nèi)的數(shù)據(jù),用一個(gè) slot 來(lái)存放,如 int,short,float 等;對(duì)于 64 位的數(shù)據(jù)用連續(xù)的兩個(gè) slot 來(lái)存放,如 long,double 等。引用類型的變量 JVM 并沒(méi)有規(guī)定其長(zhǎng)度,它可能是 32 位,也有可能是 64 位的,所以既有可能占一個(gè) slot,也有可能占兩個(gè) slot。 JVM 字節(jié)碼指令 第二個(gè)問(wèn)題,那些指令是什么意思? 指令格式 首先我們要理解 Java 指令的格式,Java 的指令以字節(jié)為單位,也就是一個(gè)字節(jié)代表一條指令。比如 iconst_1 就是一條指令,它占一個(gè)字節(jié),那么自然 Java 指令不會(huì)超過(guò) 256 條。實(shí)際上 Java 指令目前定義了 200 多條。指令雖然是一個(gè)字節(jié),但是它也可以帶自己的操作數(shù)。JVM 中有這樣一條指令 putstatic,其作用是給特定的的靜態(tài)字段賦值。但是給哪個(gè)字段賦值呢??jī)H僅通過(guò)這條指令并不能說(shuō)明,那么只有通過(guò)操作數(shù)來(lái)指定了。緊跟在 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í)行過(guò)程中動(dòng)態(tài)生成的。拿 putstatic 指令來(lái)說(shuō),它有一個(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 和 iconst3 都是由一個(gè)字節(jié)組成的字節(jié)碼指令。我們可以把 i 可以看作是指令的 “隱含操作數(shù)”,即指令本身就蘊(yùn)含了操作數(shù)。如果整數(shù) i 超過(guò) [-1, 5] 這個(gè)范圍,就不能用 iconst<i> 表示了,因?yàn)閮H一個(gè)字節(jié)的字節(jié)碼指令不可能蘊(yùn)含所有的整數(shù)。此時(shí)就需要 bipush 這條指令了,這條指令有一個(gè)嵌入式操作數(shù),由一個(gè)字節(jié)組成,用來(lái)表示要放入棧頂?shù)哪莻€(gè)整數(shù),該整數(shù)放入棧頂時(shí)通過(guò)擴(kuò)展符號(hào)位變?yōu)?32 位的整型。但是一個(gè)字節(jié)也表示不了所有的整數(shù),如果整數(shù)值超過(guò)一個(gè)字節(jié)所能表示的范圍,就只能通過(guò) ldc 這條指令了,這條指令帶有一個(gè)字節(jié)的嵌入式操作數(shù),它代表的是一個(gè)指向運(yùn)行時(shí)常量池中 Constant_Integer_info 類型常量的索引,通過(guò)索引的方式引用運(yùn)行時(shí)常量池中的整數(shù),再大的整數(shù)也不怕了。 |