十分鐘拿下JVM字節(jié)碼指令面試題
面試題
1.介紹一下你了解的一些字節(jié)碼指令
2.Java虛擬機(jī)棧的棧幀中有什么,分別介紹一下
引言
“虛擬機(jī)”是一個(gè)相對(duì)于“物理機(jī)”的概念,這兩種機(jī)器都有代碼執(zhí)行能力;
區(qū)別:
- 物理機(jī)的執(zhí)行引擎是直接建立在處理器、緩存、指令集和操作系統(tǒng)層面上
- 虛擬機(jī)的執(zhí)行引擎則是由軟件自行實(shí)現(xiàn)的,因此可以不受物理?xiàng)l件制約地定制指令集與執(zhí)行引擎的結(jié)構(gòu)體系,能夠執(zhí)行那些不被硬件直接支持的指令集格式
高級(jí)語言到機(jī)器語言
- Java代碼經(jīng)過編譯由 .java代碼源文件經(jīng)過詞法解析、語法解析、語義解析以及生成字節(jié)碼最終生成 .class字節(jié)碼文件
- 字節(jié)碼文件是二進(jìn)制文件,字節(jié)碼文件由一些JVM能夠識(shí)別的字節(jié)碼指令
- 通過JVM的執(zhí)行引擎將字節(jié)碼指令解釋/編譯為對(duì)應(yīng)平臺(tái)上的本地機(jī)器指令
- 即JVM中的執(zhí)行引擎將將高級(jí)語言翻譯為了機(jī)器語言
運(yùn)行時(shí)棧幀結(jié)構(gòu)
Java虛擬機(jī)以方法作為最基本的執(zhí)行單元,“棧幀”(Stack Frame)則是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行背后的數(shù)據(jù)結(jié)構(gòu)。
每一個(gè)方法從調(diào)用開始至執(zhí)行結(jié)束的過程,都對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧里面從入棧到出棧的過程。
每一個(gè)棧幀都包括了局部變量表、操作數(shù)棧、動(dòng)態(tài)連接、方法返回地址和一些額外的附加信息。字節(jié)碼文件中則記錄了棧幀中局部變量和操作數(shù)棧的大小。
棧幀的概念結(jié)構(gòu)如下圖所示:
操作數(shù)棧
操作數(shù)棧主要作為方法調(diào)用的中轉(zhuǎn)站使用,用于存放方法執(zhí)行過程中產(chǎn)生的中間計(jì)算結(jié)果和計(jì)算過程中產(chǎn)生的臨時(shí)變量
執(zhí)行每一條指令之前,Java 虛擬機(jī)要求該指令的操作數(shù)已被壓入操作數(shù)棧中。在執(zhí)行指令時(shí),Java 虛擬機(jī)會(huì)將該指令所需的操作數(shù)彈出,并且將指令的結(jié)果重新壓入棧中。
【例子】
調(diào)用下面代碼的fun方法,jvm執(zhí)行引擎則會(huì)先將2和4進(jìn)行入棧操作,然后分別將2和4出棧執(zhí)行指令iadd進(jìn)行加法操作,最后將結(jié)果3入棧
public int fun(){ int a = 2; int b = 4; return a+b; }
當(dāng)然上面的fun方法調(diào)用過程不止是簡單的在操作數(shù)棧進(jìn)行入棧出棧操作,還涉及到了棧幀中另外一個(gè)結(jié)構(gòu)叫做"局部變量表"
局部變量表
局部變量表用于用于存放方法參數(shù)和方法內(nèi)部定義的局部變量;
換句話說,局部變量表存放的是編譯期可知的各種數(shù)據(jù)類型(如byte short int等)和對(duì)象引用,所以在字節(jié)碼文件中也確定了局部變量表的的最大容量。
【例子】
public int fun(){ int a = 2; int b = 4; return a+b; }
繼續(xù)用上面的a+b的代碼,上面說到執(zhí)行fun方法的時(shí)候會(huì)借助操作數(shù)棧通過一些出棧入棧的操作來完成加法,但jvm執(zhí)行引擎中還用到了局部變量表,具體步驟如下(下面的步驟由javap命令所得,javap具體操作下文會(huì)介紹):
0: iconst_2 1: istore_1 2: iconst_4 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: ireturn
iconst_2指令是將int型2推送至棧頂
istore_1指令將棧頂int型數(shù)值2存入局部變量表中的第1個(gè)變量槽中
iconst_4指令將int型4推送至棧頂
istore_2指令將棧頂int型數(shù)值4存入局部變量表中的第2個(gè)變量槽中
iload_1指令的作用是將局部變量表第1個(gè)變量槽中的整型值復(fù)制到操作數(shù)棧頂
iload_2指令的作用是將局部變量表第1個(gè)變量槽中的整型值復(fù)制到操作數(shù)棧頂
iadd指令將操作數(shù)棧的棧頂兩個(gè)元素出棧,進(jìn)行相加,將結(jié)果 6 重新入棧
也許會(huì)覺得上面這樣把數(shù)從操作數(shù)棧放到局部變量表又從局部變量表復(fù)制到操作數(shù)棧是不是有點(diǎn)多余,其實(shí)一點(diǎn)也不多余,如下代碼所示,在第四行新增了一個(gè)加法操作,這樣應(yīng)該就能夠看出局部變量表的作用了
public int fun(){ int a = 2; int b = 4; int c = a*b; return b+c; }
像a和b變量如果只存在于操作數(shù)棧,那么執(zhí)行了乘法操作之后,再次想執(zhí)行b+c的時(shí)候,我們的操作數(shù)棧已經(jīng)沒有變量b了,所以需要一個(gè)局部變量表,存儲(chǔ)變量以及運(yùn)算過程產(chǎn)生的中間變量
操作數(shù)棧的相關(guān)指令如下:
其他的基本數(shù)據(jù)類型的加載和存儲(chǔ)的指令如下:
剛剛的字節(jié)碼執(zhí)行執(zhí)行到最后,還有一個(gè)ireturn指令
ireturn指令是方法返回指令之一他將結(jié)束方法,并將操作數(shù)棧的棧頂元素返回給方法的調(diào)用者
動(dòng)態(tài)連接
每一個(gè)棧幀內(nèi)部都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用,包含這個(gè)引用的目的就是為了支持當(dāng)前方法的代碼能夠?qū)崿F(xiàn)動(dòng)態(tài)鏈接(Dynamic Linking)
在Java源文件被編譯到字節(jié)碼文件時(shí),所有的變量和方法引用都作為符號(hào)引用(Symbilic Reference)保存在class文件的常量池里
這些符號(hào)引用一部分會(huì)在類加載階段或者第一次使用的時(shí)候就被轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化被稱為靜態(tài)解析。 另外一部分將在每一次運(yùn)行期間都轉(zhuǎn)化為直接引用,這部分就稱為動(dòng)態(tài)連接。
方法返回地址
當(dāng)一個(gè)方法開始執(zhí)行后,只有兩種方式退出這個(gè)方法。第一種方式是執(zhí)行引擎遇到任意一個(gè)方法返回的字節(jié)碼指令,這時(shí)候可能會(huì)有返回值傳遞給上層的方法調(diào)用者,這種退出方法的方式稱為“正常調(diào)用完成”(Normal Method Invocation Completion)例如上面fun方法就是通過ireturn指令結(jié)束方法調(diào)用并返回值。
相關(guān)的返回字節(jié)碼指令如下:
另外一種退出方式是在方法執(zhí)行的過程中遇到了異常,并且這個(gè)異常沒有在方法體內(nèi)得到妥善處理。這種退出方法的方式稱為“異常調(diào)用完成(Abrupt Method Invocation Completion)”。一個(gè)方法使用異常完成出口的方式退出,是不會(huì)給它的上層調(diào)用者提供任何返回值的。
無論采用何種退出方式,在方法退出之后,都必須返回到最初方法被調(diào)用時(shí)的位置,程序才能繼 續(xù)執(zhí)行。
- 方法正常退出時(shí),主調(diào)方法的PC計(jì)數(shù)器的值就可以作為返回地址
- 方法異常退出時(shí),返回地址是要通過異常處理器表來確定的
Javac和Javap查看字節(jié)碼指令
javac即java compiler,能夠?qū)⒃创a編譯為字節(jié)碼文件
javap是JDK自帶的反匯編器,可以查看java編譯器生成的字節(jié)碼
在Java項(xiàng)目下新建一個(gè)Test類,代碼如下
public class Test { public int fun(){ int a = 2; int b = 4; return a+b; } public static void main(String[] args) { Test test = new Test(); test.fun(); } }
打開控制臺(tái)Terminal,將地址切換到Test.java所在目錄
執(zhí)行javac Test.java將Test編譯為class文件,然后執(zhí)行javap -v Test.class,查看字節(jié)碼指令
找到對(duì)應(yīng)fun方法下對(duì)應(yīng)的字節(jié)碼指令
這樣就得到了字節(jié)碼指令
在fun字節(jié)碼指令上面,會(huì)發(fā)現(xiàn)invokespecial指令,用于調(diào)用實(shí)例構(gòu)造器<init>()方法、私有方法和父類中的方法
其它方法調(diào)用指令:
invokestatic。用于調(diào)用靜態(tài)方法。
invokespecial。用于調(diào)用實(shí)例構(gòu)造器<init>()方法、私有方法和父類中的方法。
invokevirtual。用于調(diào)用所有的虛方法。
invokeinterface。用于調(diào)用接口方***在運(yùn)行時(shí)再確定一個(gè)實(shí)現(xiàn)該接口的對(duì)象。
invokedynamic:動(dòng)態(tài)解析出需要調(diào)用的方法,然后執(zhí)行
非虛方法概念:
如果方法在編譯期就確定具體調(diào)用版本,這個(gè)版本在運(yùn)行期間是不可變的。
靜態(tài)方法、私有方法、final方法、實(shí)例構(gòu)造器、父類方法都是非虛方法,其它方法都為虛方法。
但是final方法是由invokevirtual指令調(diào)用
Java字節(jié)碼指令
操作數(shù)棧專用指令
基本數(shù)據(jù)類型的加載和存儲(chǔ)指令:
運(yùn)算相關(guān)指令
數(shù)組相關(guān)指令
- 新建基本類型數(shù)組 newarray
- 新建引用類型數(shù)組 anewarray
- 生成多維數(shù)組 multianewarray
- 求數(shù)組長度 arraylength
數(shù)組的加載指令以及存儲(chǔ)指令:
總結(jié)
全文從Java虛擬運(yùn)行時(shí)棧幀的結(jié)構(gòu),引出了一些常見的Java字節(jié)碼指令,如操作數(shù)棧相關(guān)指令、基本數(shù)據(jù)加載和存儲(chǔ)指令、數(shù)組相關(guān)指令以及方法調(diào)用指令,但值得注意的是想iconst、iload、iadd這些字母形式的字節(jié)碼叫做”助記符“只是方便理解,在字節(jié)碼文件實(shí)際存儲(chǔ)的是十六進(jìn)制。如 iadd對(duì)應(yīng)存儲(chǔ)的是 0x60。
字節(jié)碼指令表:https://www.yuque.com/docs/share/b1ec3861-b3b2-40f2-9e19-a1ca22ff50ba
相關(guān)面試題:
1.介紹一下你了解的一些字節(jié)碼指令
2.Java虛擬機(jī)棧的棧幀中有什么,分別介紹一下
參考:
《深入理解Java虛擬機(jī) 第三版》周志明
《深入拆解 Java 虛擬機(jī)》鄭雨迪
#Java##Java面試##面試##java面試題#