自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

字節(jié)一面:能聊聊字節(jié)碼么?

開(kāi)發(fā) 前端
棧空間是有限的,如果只有入棧沒(méi)有出棧,最后必然會(huì)出現(xiàn)空間不足,同時(shí)也就會(huì)報(bào)出經(jīng)典的StackOverflowError(棧溢出錯(cuò)誤),最常見(jiàn)的導(dǎo)致棧溢出的情況就是遞歸函數(shù)里忘了寫(xiě)終止條件。

1.前言

上一篇《??你能和我聊聊Class文件么??》中,我們對(duì)Class文件的各個(gè)部分做了簡(jiǎn)單的介紹,當(dāng)時(shí)留了一個(gè)很重要的部分沒(méi)講,不是敖丙不想講啊,而是這一部分實(shí)在太重要了,不獨(dú)立成篇好好zhejinrong 講講都對(duì)不起詹姆斯·高斯林。

這最重要的部分當(dāng)然就是字節(jié)碼啦。

先來(lái)個(gè)定義:Java字節(jié)碼是一組可以由Java虛擬機(jī)(JVM)執(zhí)行的高度優(yōu)化的指令,它被記錄在Class文件中,在虛擬機(jī)加載Class文件時(shí)執(zhí)行。

說(shuō)大白話就是,字節(jié)碼是Java虛擬機(jī)能夠看明白的可執(zhí)行指令。

前面的文章中已經(jīng)強(qiáng)調(diào)了很多次了,Class文件不等于字節(jié)碼,為什么我要一直強(qiáng)調(diào)這個(gè)事情呢?

因?yàn)樵诮^大部分的中文資料和博客中,這兩個(gè)東西都被嚴(yán)重的弄混了...

導(dǎo)致現(xiàn)在一說(shuō)字節(jié)碼大家就會(huì)以為和Class文件是同一個(gè)東西,甚至有的文章直接把Class文件稱為“字節(jié)碼”文件。

這樣的理解顯然是有偏差的。

舉個(gè)例子,比如我們所熟知的.exe可執(zhí)行文件,.exe文件中包含機(jī)器指令,但除了機(jī)器指令之外,.exe文件還包含其他與準(zhǔn)備執(zhí)行這些指令相關(guān)的信息。

因此我們不能說(shuō)“機(jī)器指令”就是.exe文件,也不能把.exe文件稱為“機(jī)器指令”文件,它們只是一種包含關(guān)系,僅此而已。

同樣的,Class文件并不等于字節(jié)碼,只能說(shuō)Class文件包含字節(jié)碼。

上次的文章中我們提到,字節(jié)碼(或者稱為字節(jié)碼指令)被存儲(chǔ)在Class文件中的方法表中,它以Code屬性的形式存在。

因此,可以通俗地說(shuō),字節(jié)碼就是Class文件方法表(methods)中的Code屬性。

今天我們來(lái)好好聊聊字節(jié)碼。

但是在講字節(jié)碼知識(shí)之前我們需要對(duì)Java虛擬機(jī)(Java Virtual Machine,簡(jiǎn)稱JVM)的內(nèi)部結(jié)構(gòu)有一個(gè)簡(jiǎn)單的理解,畢竟字節(jié)碼說(shuō)到底指示虛擬機(jī)各個(gè)部分需要執(zhí)行什么操作的命令,先簡(jiǎn)單了解JVM,知己知彼方能百戰(zhàn)百勝。

2.JVM的內(nèi)部結(jié)構(gòu)

我們借這么一張圖來(lái)稍微聊聊JVM執(zhí)行Class文件的流程。

這是學(xué)習(xí)JVM過(guò)程中躲不開(kāi)的一張圖,當(dāng)然我們今天不講那么深。

字節(jié)碼是對(duì)方法執(zhí)行過(guò)程的抽象,于是我們今天只把跟方法執(zhí)行過(guò)程最直接相關(guān)的幾個(gè)部分拎出來(lái)講講。

其實(shí)虛擬機(jī)執(zhí)行代碼時(shí),虛擬機(jī)中的每一部分都需要參與其中,但本篇我們更關(guān)注的是跟"執(zhí)行過(guò)程"相關(guān)的幾個(gè)部分,也就是跟代碼順序執(zhí)行這一動(dòng)態(tài)過(guò)程相關(guān)的幾個(gè)部分。有點(diǎn)云里霧里了嗎,不要急,往下看。

以Hello.class作為今天的主角。

當(dāng)Hello.class被加載時(shí),首先經(jīng)歷的是Class文件中的信息被加載到JVM方法區(qū)中的過(guò)程。

方法區(qū)是什么?

方法區(qū)是存儲(chǔ)方法運(yùn)行相關(guān)信息的一個(gè)區(qū)域。

如果把Class文件中的信息理解為一顆顆的子彈,那么方法區(qū)就可以看做是成JVM的"彈藥庫(kù)",而將Class文件中的信息加載到方法區(qū)這一過(guò)程相當(dāng)于“子彈上膛”。

只有當(dāng)子彈上膛后,JVM才具備了“開(kāi)火”的能力,這很合理吧。

例如,原本記錄在Class文件中的常量池,此時(shí)被加載到方法區(qū)中,成為運(yùn)行時(shí)常量池。同時(shí),字節(jié)碼指令也被裝配到方法區(qū)中,為方法的運(yùn)行提供支持。

類加載動(dòng)圖

當(dāng)類Hello.class被加載到方法區(qū)后,JVM會(huì)為Hello這個(gè)類在堆上新建一個(gè)類對(duì)象。

第二個(gè)知識(shí)點(diǎn)來(lái)咯:堆是 放置對(duì)象實(shí)例的地方,所有的對(duì)象實(shí)例以及數(shù)組都應(yīng)當(dāng)在運(yùn)行時(shí)分配在堆上。

一般在執(zhí)行新建對(duì)象相關(guān)操作時(shí)(例如 new HashMap),才會(huì)在堆上生成對(duì)象。

但是你看,我們明明還沒(méi)開(kāi)始執(zhí)行代碼呢,這才剛處于類的加載階段,堆上就開(kāi)始進(jìn)行對(duì)象分配了,難道有什么特殊的對(duì)象實(shí)例在類加載的時(shí)候就被創(chuàng)建了嗎?

沒(méi)錯(cuò),這個(gè)實(shí)例的確特殊,它就是我們?cè)诜瓷鋾r(shí)常常會(huì)用到的 java.lang.Class對(duì)象!!!

如果你忘了什么是反射的話,我來(lái)提醒你一下:

Hello obj = new Hello();
Class<?> clz = obj.getClass();

在Hello這個(gè)類的Class文件被加載到方法區(qū)的之后,JVM就在堆區(qū)為這個(gè)新加載的Hello類建立了一個(gè)java.lang.Class實(shí)例。

說(shuō)到這里,你對(duì)”Java是一門(mén)面向?qū)ο蟮恼Z(yǔ)言“這句話有沒(méi)有更深入的理解——在Java中,即使連類也是作為對(duì)象而存在的。

不僅如此,由于JDK 7之后,類的靜態(tài)變量存放在該類對(duì)應(yīng)的java.lang.Class對(duì)象中。因此當(dāng) java.lang.Class在堆上分配好之后,靜態(tài)變量也將被分配空間,并獲得最初的零值。

注意,這里的零值指的不是靜態(tài)變量初始化哦,僅僅只是在類對(duì)象空間分配后,JVM為所有的靜態(tài)變量賦了一個(gè)用于占位的零值,零值很好理解嘛,也就是數(shù)值對(duì)象被設(shè)為0,引用類型被設(shè)為null。

到這里為止,類的信息已經(jīng)完全準(zhǔn)備好了,接下來(lái)要開(kāi)始的,就是執(zhí)行方法。我們?cè)凇禞ava代碼編譯流程是怎樣的》一文中討論過(guò),方法是類的構(gòu)造方法,它的作用是初試化類中所有的靜態(tài)變量并執(zhí)行用static {}包裹的代碼塊,而且該方法的收集是有順序的:

  • 父類靜態(tài)變量初始化 及 父類靜態(tài)代碼塊;
  • 子類靜態(tài)變量初始化 及 子類靜態(tài)代碼塊。

<clinit>方法相當(dāng)于是把靜態(tài)的代碼打包在一起執(zhí)行,而且函數(shù)是在編譯時(shí)就已經(jīng)將這些與類相關(guān)的初始化代碼按順序收集在一起了,因此在Class文件中可以看到函數(shù):

當(dāng)然,如果類中既沒(méi)有靜態(tài)變量,也沒(méi)有靜態(tài)代碼塊,則不會(huì)有函數(shù)。

總之,如果函數(shù)存在,那么在類被加載到JVM之后,函數(shù)開(kāi)始執(zhí)行,初始化靜態(tài)變量。

接下來(lái)我們今天最重要的部分要登場(chǎng)了!!!

就決定是你了,虛擬機(jī)棧!!

第三個(gè)知識(shí)點(diǎn):虛擬機(jī)棧是線程中的方法的內(nèi)存模型。

上面這句話聽(tīng)著很抽象是吧,沒(méi)事,我來(lái)好好解釋一下。

首先要明白的是,虛擬機(jī)棧,顧名思義是用棧結(jié)構(gòu)實(shí)現(xiàn)的一種的線性表,其限制是僅允許在表的同一端進(jìn)行插入和刪除運(yùn)算,這一端被稱為棧頂,相對(duì)地,把另一端稱為棧底。

棧的特性是每次操作都是從棧頂進(jìn)或者從棧頂出,且滿足先進(jìn)后出的順序,而虛擬機(jī)棧也繼承了這一優(yōu)良傳統(tǒng)。

虛擬機(jī)棧是與方法執(zhí)行最直接相關(guān)的一個(gè)區(qū)域,用于記錄Java方法調(diào)用的“活動(dòng)記錄”(activation record)。

虛擬機(jī)棧以棧幀(frame)為單位線程的運(yùn)行狀態(tài),每調(diào)用一個(gè)方法就會(huì)分配一個(gè)新的棧幀壓入Java棧上,每從一個(gè)方法返回則彈出并撤銷相應(yīng)的棧幀。

例如,這么一段代碼:

public class Hello {
public static int a = 0;
public static void main(String[] args) {
add(1,2);
}

public static int add(int x,int y) {
int z = x+y;
System.out.println(z);
return z;
}
}

它的調(diào)用鏈如下:

調(diào)用鏈

現(xiàn)在你明白了吧,代碼中層層調(diào)用的概念在JVM里是使用棧數(shù)據(jù)結(jié)構(gòu)來(lái)實(shí)現(xiàn)的,調(diào)用方法時(shí)生成棧幀并入棧,方法執(zhí)行完出棧,直到所有方法都出棧了,就意味著整個(gè)調(diào)用鏈結(jié)束。

還記得二叉樹(shù)的前序遍歷怎么寫(xiě)的嗎:

public void preOrderTraverse(TreeNode root) {
if (root != null) {
System.out.print(root.val + "->");
preOrderTraverse(root.left);
preOrderTraverse(root.right);
}
}

這種遞歸形式本質(zhì)上就是利用虛擬機(jī)棧對(duì)同一個(gè)方法的遞歸入棧實(shí)現(xiàn)的,如果我們寫(xiě)成非遞歸形式的前序遍歷,應(yīng)該是這樣子的:

public void preOrderTraverse(TreeNode root) {
// 自己聲明一個(gè)棧
Stack<TreeNode> stack = new Stack<>();
TreeNode node = root;
while (node != null || !stack.empty()) {
if (node != null) {
System.out.print(node.val + "->");
stack.push(node);
node = node.left;
} else {
TreeNode tem = stack.pop();
node = tem.right;
}
}
}

二叉樹(shù)遍歷的非遞歸形式就是由我們自己把棧寫(xiě)好,并實(shí)現(xiàn)出棧入棧的功能,跟遞歸方式調(diào)用的本質(zhì)是相似的,只不過(guò)遞歸操作中我們依賴虛擬機(jī)棧來(lái)執(zhí)行入棧出棧。

總之,靠棧可以很好地表達(dá)方法間的這種層層調(diào)用的層級(jí)關(guān)系。

當(dāng)然,棧空間是有限的,如果只有入棧沒(méi)有出棧,最后必然會(huì)出現(xiàn)空間不足,同時(shí)也就會(huì)報(bào)出經(jīng)典的StackOverflowError(棧溢出錯(cuò)誤),最常見(jiàn)的導(dǎo)致棧溢出的情況就是遞歸函數(shù)里忘了寫(xiě)終止條件。

其次,多個(gè)線程的方法執(zhí)行應(yīng)當(dāng)為獨(dú)立且互不干擾的,因此每一個(gè)線程都擁有自己獨(dú)立的一個(gè)虛擬機(jī)棧。

這也導(dǎo)致了各個(gè)線程之間方法的執(zhí)行速度并不能保持一致,有時(shí)A線程先執(zhí)行完,有時(shí)B線程先執(zhí)行完,究其原因就是因?yàn)樘摂M機(jī)棧是線程私有,各自獨(dú)立執(zhí)行。

談完了虛擬機(jī)棧的整體情況,我們?cè)賮?lái)看看虛擬機(jī)棧中的棧幀。

棧幀是虛擬機(jī)棧中的基礎(chǔ)元素,它隨著方法的調(diào)用而創(chuàng)建,記錄了被調(diào)用方法的運(yùn)行需要的重要信息,并隨著方法的結(jié)束而消亡。

那么你就要問(wèn)了,棧幀里到底包裹了些什么東西呀?

好的同學(xué),等我把這個(gè)問(wèn)題回答完,今天的知識(shí)你至少就懂了一半。

3.棧幀的組成

棧幀主要由以下幾個(gè)部分組成:

  • 局部變量表
  • 操作數(shù)棧
  • 動(dòng)態(tài)連接
  • 方法出口
  • 其他信息

3.1 局部變量表

局部變量表(Local Variable Table)是一個(gè)用于存儲(chǔ)方法參數(shù)和方法內(nèi)部定義的局部變量的空間。

一個(gè)重要的特性是,在Java代碼被編譯為Class文件時(shí),就已經(jīng)確定了該方法所需要分配的局部變量表的最大容量。

也就是說(shuō),早在代碼編譯階段,就已經(jīng)把局部變量表需要分配的大小計(jì)算好了,并記錄在Class文件中,例如:

public class Hello {
public static void main(String[] args) {
for (int i=0;i<3;i++){
System.out.printf(i+"");
}
}
}

這個(gè)類的main方法,通過(guò)javap之后可以得到其中的局部變量表:

LocalVariableTable:
Start Length Slot Name Signature
2 41 1 i I
0 44 0 args [Ljava/lang/String;

這個(gè)意思就是告訴你,這個(gè)方法會(huì)產(chǎn)生兩個(gè)局部變量,Slot代表他們?cè)诰植孔兞勘碇械南聵?biāo)。

難道方法里定義了多少個(gè)局部變量,局部變量表就會(huì)分配多少個(gè)Slot坑位嗎?

不不不,編譯器精明地很,它會(huì)采取一種稱為Slot復(fù)用的方法來(lái)節(jié)省空間,舉個(gè)例子,我們?yōu)榍懊娴姆椒ㄔ僭黾右粋€(gè)for循環(huán):

public class Hello {
public static void main(String[] args) {
for (int i=0;i<3;i++){
System.out.printf(i+"");
}
for (int j=0;j<3;j++){
System.out.printf(j+"");
}
}
}

然后會(huì)得到如下局部變量表:

LocalVariableTable:
Start Length Slot Name Signature
2 41 1 i I
45 41 1 j I
0 87 0 args [Ljava/lang/String;

雖然還是三個(gè)變量,但是i和j的Slot是同一個(gè),也就是說(shuō),他們共用了同一個(gè)下標(biāo),在局部變量表中占的是同一個(gè)坑位。

至于原因呢,相信聰明的你已經(jīng)看出來(lái)了,跟局部變量的作用域有關(guān)系。

變量i作用域是第一個(gè)for循環(huán)的內(nèi)部,而當(dāng)變量j創(chuàng)建時(shí),i的生命周期就已經(jīng)結(jié)束了。因此j可以復(fù)用i的Slot將其覆蓋掉,以此來(lái)節(jié)省空間。

所以,雖然看起來(lái)創(chuàng)建了三個(gè)局部變量,但其實(shí)只需要分配兩個(gè)變量的空間。

3.2 操作數(shù)棧

棧幀中的第二個(gè)重要部分是操作數(shù)棧。

等等,這怎么又來(lái)了個(gè)棧,擱這套娃呢???

沒(méi)辦法呀,棧這玩意實(shí)在太好用了,首先棧的基本操作非常簡(jiǎn)單,只有入棧和出棧兩種,這個(gè)優(yōu)勢(shì)可以保證每一條JVM的指令都代碼緊湊且體積小;其次棧用來(lái)求值也是非常經(jīng)典的用法,簡(jiǎn)單又方便喔。

也有一種基于寄存器的體系結(jié)構(gòu),將局部變量表與操作數(shù)棧的功能組合在一起,關(guān)于這兩種體系優(yōu)劣勢(shì)的詳細(xì)討論可以移步至R大的博客:https://www.iteye.com/blog/rednaxelafx-492667

至于用棧來(lái)求值這種用法,大家在《數(shù)據(jù)結(jié)構(gòu)》課上學(xué)棧這一結(jié)構(gòu)的時(shí)候應(yīng)該都接觸過(guò)了,這里不多展開(kāi)。如果沒(méi)有印象了,建議看看Leetcode上的這一題:https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/

總之,情況就是這么個(gè)情況,虛擬機(jī)棧的每一個(gè)棧幀里都包含著一個(gè)操作數(shù)棧,作用是保存求值的中間結(jié)果和調(diào)用別的方法的參數(shù)等。

3.3 動(dòng)態(tài)連接

動(dòng)態(tài)連接這個(gè)名詞在全網(wǎng)的JVM中文資料中解釋得非?;靵y,在你基礎(chǔ)沒(méi)有打牢之前不建議你深入去細(xì)究,腦子會(huì)亂掉的。

我這里會(huì)給大家一個(gè)非常通俗易懂的解釋,了解即可。

首先,棧幀中的這個(gè)動(dòng)態(tài)連接,英文是Dynamic Linking,Linking在這里是作為名詞存在的,跟前面的表、棧是同一個(gè)層次的東西。

這個(gè)連接說(shuō)白了就是棧幀的當(dāng)前方法指向運(yùn)行時(shí)常量池的一個(gè)引用。

為什么需要有這個(gè)引用呢?

前面說(shuō)了,Class文件中關(guān)鍵信息都保存在方法區(qū)中,所以方法執(zhí)行的時(shí)候生成的棧幀得知道自己執(zhí)行的是哪個(gè)方法,靠的就是這個(gè)動(dòng)態(tài)連接直接引用了方法區(qū)中該方法的實(shí)際內(nèi)存位置,然后再根據(jù)這個(gè)引用,讀取其中的字節(jié)碼指令。

至于"動(dòng)態(tài)"二字,牽扯到的就是Java的繼承和多態(tài)的機(jī)制,有的類繼承了其他的類并重寫(xiě)了父類中的方法,因此在運(yùn)行時(shí),需要"動(dòng)態(tài)地"識(shí)別應(yīng)該要連接的實(shí)際的類、以及需要執(zhí)行的具體的方法是哪一個(gè)。

3.4 方法出口

當(dāng)一個(gè)方法開(kāi)始執(zhí)行,只有兩種方式退出這個(gè)方法,第一種方式是正常返回,即遇到了return語(yǔ)句,另一種方式則是在執(zhí)行中遇到了異常,需要向上拋出。

無(wú)論是那種形式的返回,在此方法退出之后,虛擬機(jī)棧都應(yīng)該退回到該方法被上層方法調(diào)用時(shí)的位置。

棧幀中的方法出口記錄的就是被調(diào)用的方法退出后應(yīng)該回到上層方法的什么位置。

好了,到這里為止,棧幀中的內(nèi)容就介紹結(jié)束了,接下來(lái)我們用一個(gè)簡(jiǎn)單的例子來(lái)了解字節(jié)碼指令,以及執(zhí)行執(zhí)行時(shí)JVM各區(qū)域的運(yùn)行過(guò)程。

4.實(shí)例:++i與i++的字節(jié)碼實(shí)例

public class Hello {
public static int a = 0;
public static void main(String[] args) {
int b = 0;
b = b++;
System.out.println(b);
b = ++b;
System.out.println(b);
a = a++;
System.out.println(a);
a = ++a;
System.out.println(a);
}
}

這段程序的輸出會(huì)是是這樣的:

0
1
0
1

這是初學(xué)Java時(shí)一道經(jīng)典的誤導(dǎo)題,大家可能已經(jīng)知其然,一眼就能看出正確的結(jié)果,可對(duì)于最底層的原理卻未必知其所以然。

b=b++執(zhí)行完后變量b并沒(méi)有發(fā)生變化,只有在b=++b時(shí)變量b才自增成功。

這里其實(shí)涉及到自增操作在字節(jié)碼層面的實(shí)現(xiàn)問(wèn)題。

我們先來(lái)看看這一段代碼對(duì)應(yīng)的字節(jié)碼是怎樣的,使用jclasslib來(lái)查看Hello類的main方法中的Code屬性:

將Code中的信息粘貼出來(lái):

 0 iconst_0
1 istore_1
2 iload_1
3 iinc 1 by 1
6 istore_1
7 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
10 iload_1
11 invokevirtual #3 <java/io/PrintStream.println : (I)V>
14 iinc 1 by 1
17 iload_1
18 istore_1
19 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
22 iload_1
23 invokevirtual #3 <java/io/PrintStream.println : (I)V>
26 getstatic #4 <com/cc/demo/Hello.a : I>
29 dup
30 iconst_1
31 iadd
32 putstatic #4 <com/cc/demo/Hello.a : I>
35 putstatic #4 <com/cc/demo/Hello.a : I>
38 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
41 getstatic #4 <com/cc/demo/Hello.a : I>
44 invokevirtual #3 <java/io/PrintStream.println : (I)V>
47 getstatic #4 <com/cc/demo/Hello.a : I>
50 iconst_1
51 iadd
52 dup
53 putstatic #4 <com/cc/demo/Hello.a : I>
56 putstatic #4 <com/cc/demo/Hello.a : I>
59 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
62 getstatic #4 <com/cc/demo/Hello.a : I>
65 invokevirtual #3 <java/io/PrintStream.println : (I)V>
68 return

Emmm....看起來(lái)有點(diǎn)密密麻麻,不知道該從哪看起。

其實(shí)閱讀字節(jié)碼指令是有技巧的,字節(jié)碼和源碼的對(duì)應(yīng)關(guān)系已經(jīng)記錄在了字節(jié)碼中,也就是Code屬性中的LineNumberTable,這里記錄的是源碼的行號(hào)和字節(jié)碼行號(hào)的對(duì)應(yīng)關(guān)系。

如圖,右側(cè)的起始PC指的是字節(jié)碼的起始行號(hào),行號(hào)則是字節(jié)碼對(duì)應(yīng)的源碼行號(hào)。

將這個(gè)例子中的源碼和字節(jié)碼對(duì)應(yīng)起來(lái)的效果如圖所示:

這么一對(duì)應(yīng),是不是就清晰很多了?

掌握了這個(gè)技巧之后我們就可以開(kāi)始分析整體的流程和細(xì)節(jié)了。

4.1 靜態(tài)變量賦值

首先來(lái)捋一捋,當(dāng)Hello類加載到JVM之后發(fā)生了什么,按我們前面說(shuō)的,加載完成之后,虛擬機(jī)棧需要進(jìn)行方法入棧,而眾所周知,main方法是執(zhí)行的入口,所以main方法最先入棧。

但是,是這樣的嗎?

別忘記了這一行代碼:

靜態(tài)變量的賦值需要在main方法之前執(zhí)行,前面已經(jīng)提到了,靜態(tài)變量的賦值操作被封裝在方法中。

因此,**方法需要先于main方法入棧執(zhí)行**,在本例中,方法長(zhǎng)這樣:

當(dāng)然,方法的LineNumberTable也記錄了字節(jié)碼跟源碼的對(duì)應(yīng)關(guān)系,只不過(guò)在這里對(duì)應(yīng)源碼只有一行:

因此public static int a = 0;這一行源代碼就對(duì)應(yīng)了三行的字節(jié)碼:

0 iconst_0
1 putstatic #4 <com/cc/demo/Hello.a : I>
4 return

簡(jiǎn)直沒(méi)有比這更適合作為字節(jié)碼教學(xué)入門(mén)素材的了!

接下來(lái)就可以開(kāi)始愉快地手撕字節(jié)碼了。

第一句iconst_0,在官方的JVM規(guī)范中是這么解釋的:“Push the int constant onto the operand stack”,也就是說(shuō)iconst操作是把一個(gè)int類型的常量數(shù)據(jù)壓入到操作數(shù)棧的棧頂。

這個(gè)指令開(kāi)頭的字母表示的是類型,在本例中i代表int。我們可以舉一反三,當(dāng)然還會(huì)有l(wèi)const代表把long類型的常量入棧到棧頂,有fconst指令表示把float類型的常量推到棧頂?shù)鹊鹊鹊取?/p>

這個(gè)指令結(jié)尾的數(shù)字就是需要入棧的值了~

恭喜你,看完上面這段話,你至少已經(jīng)學(xué)會(huì)了n種字節(jié)碼指令了。

不就是排列組合嘛,so easy!

再來(lái)看第二句,putstatic #4,光看字面意思就能很容易的猜出它的作用,這個(gè)指令的含義是:當(dāng)前操作數(shù)棧頂出棧,并給靜態(tài)字段賦值。

把剛才放到操作數(shù)棧頂?shù)?拿出來(lái),賦值給常量池中#4位置字面量表示的靜態(tài)變量,這里可以看到#4位置的字面量就是。

所以,這第二行字節(jié)碼,本質(zhì)上是一個(gè)賦值操作,將0這個(gè)值賦給了靜態(tài)變量a。

靜態(tài)變量存儲(chǔ)在堆中該類對(duì)應(yīng)的Class對(duì)象實(shí)例中,也就是我們?cè)诜瓷錂C(jī)制中用對(duì)應(yīng)類名拿到的那個(gè)Class對(duì)象實(shí)例。

最后一行是一個(gè)return,這個(gè)沒(méi)啥好說(shuō)的。

好了,這就是本例中的方法中的全部了,并不難吧。

當(dāng)<clinit>方法執(zhí)行完出棧后,main方法入棧,開(kāi)始執(zhí)行main方法Code屬性中的字節(jié)碼指令。

為了方便講解,接下來(lái)我會(huì)逐行將源碼與其對(duì)應(yīng)的字節(jié)碼貼在一起。

4.2 局部變量賦值

首先是源碼中的第六行 ,也就是main函數(shù)的第一句:

//Source code
int b = 0;

//Byte code
0 iconst_0
1 istore_1

這一句源碼對(duì)應(yīng)了兩行字節(jié)碼。

其中,iconst_0這個(gè)在前面已經(jīng)講過(guò)了,將int類型的常量從棧頂壓入,由于此時(shí)操作數(shù)棧為空,所以0被壓入后理所當(dāng)然地既是棧頂,也是棧底。

然后是istore_1命令,這個(gè)跟iconst_0的結(jié)構(gòu)很像,以一個(gè)類型縮寫(xiě)開(kāi)頭,以一個(gè)數(shù)字結(jié)尾,那么我們只要弄清楚store的含義就行了,store表示將棧頂?shù)膶?duì)應(yīng)類型元素出棧,并保存到局部變量表指定位置中。

由于此時(shí)的棧頂元素就是剛才壓入的int類型的0,所以我們要存儲(chǔ)到局部變量表中的就是這個(gè)0。

那么問(wèn)題來(lái)了,這個(gè)值需要放到局部變量表中的哪個(gè)位置呢?

在iconst_0命令中,末尾的數(shù)字代表需要入棧的常量,但在istore_1命令中,操作數(shù)是從操作數(shù)棧中取出的,是不用聲明的,那istore_1命令末尾這個(gè)數(shù)字的用途是什么呢?

前面說(shuō)了,store表示將棧頂?shù)膶?duì)應(yīng)類型元素保存到局部變量表指定位置中。

因此iconst_0指令末尾這個(gè)數(shù)字代表就是指定位置啦,也就是局部變量表的下標(biāo)。

從LocalVariableTable中可以看出,下標(biāo)為1的位置中存儲(chǔ)的就是局部變量b。

下標(biāo)0位置存儲(chǔ)的是方法的入?yún)ⅰ?/p>

總之,istore_1這個(gè)命令就意味著棧頂?shù)膇nt元素出棧,并保存到局部變量表下標(biāo)為1的位置中。

同樣的,stroe這個(gè)命令也可以與各種類型縮寫(xiě)的開(kāi)頭組合成不同的命令,像什么lstroe、fstore等等。

ok,這又是一個(gè)經(jīng)典的聲明和賦值操作。

4.3 局部變量

自增4.3.1 i++過(guò)程

我們繼續(xù)往下看,源碼第七行和它對(duì)應(yīng)的字節(jié)碼:

//Source code
b = b++;

//Byte code
2 iload_1
3 iinc 1 by 1
6 istore_1

首先是iload_1命令,這個(gè)命令是與istore_1命令對(duì)應(yīng)的反向命令。

store不是從操作數(shù)棧棧頂取數(shù)存到局部變量表中嘛,那么load要做的事情恰恰相反,它做的是從局部變量表指定位置中取數(shù)值,并壓入到操作數(shù)棧的棧頂。

那么iload_1詳細(xì)來(lái)說(shuō)就是:從局部變量表的位置1中取出int類型的值,并壓入操作數(shù)棧。

但是,這里的取值操作其實(shí)是一個(gè)“拷貝”操作:從局部變量表中取出一個(gè)數(shù),其實(shí)是將該值復(fù)制一份,然后壓入操作數(shù)棧,而局部變量表中的數(shù)值還保存著,沒(méi)有消失。

然后是一個(gè)iinc 1 by 1指令,這是一個(gè)雙參數(shù)指令,主要的功能是將局部變量表中的值自增一個(gè)常量值。

iinc指令的第一個(gè)參數(shù)值的含義是局部變量表下標(biāo),第二個(gè)參數(shù)值需要增加的常量值。

因此**iinc 1 by 1就表示局部變量表中下標(biāo)為1位置的值增加1。**

再來(lái)看第三條指令istore_1,這個(gè)很熟悉了,操作數(shù)棧棧頂元素出棧,存到局部變量表中下標(biāo)為1的位置。

等等,是不是有什么奇怪的事情發(fā)生了。

iinc 1 by 1就表示局部變量表中下標(biāo)為1位置的值由0變成了1,但是istore_1把一開(kāi)始從局部變量表下標(biāo)1復(fù)制到操作數(shù)棧的0值又賦值到了下標(biāo)位置1。

因此無(wú)論中間局部變量表中的對(duì)應(yīng)元素做了什么操作,到了這一步都直接白費(fèi)功夫,相當(dāng)于是脫褲子放屁了。

來(lái)個(gè)動(dòng)圖,看得更清晰:

局部變量b++流程

因此b = b++從字節(jié)碼上來(lái)看,自增后又被初始值覆蓋了,最終自增失敗。

繼續(xù)看下一句:

//Source code
System.out.println(b);

//Byte code
7 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
10 iload_1
11 invokevirtual #3 <java/io/PrintStream.println : (I)V>

這一句是與控制臺(tái)打印有關(guān)的字節(jié)碼,與今天的主題聯(lián)系不大,稍微過(guò)一下即可。

getstatic #2是獲取常量池中索引為#2的字面量對(duì)應(yīng)的靜態(tài)元素。

iload_1 從局部變量表中索引為1的位置取數(shù)值,并壓入到操作數(shù)棧的棧頂,這里取的就是變量b的值啦。

然后最后一句是invokevirtual #3,invoke這個(gè)單詞我們?cè)诖砟J街幸步?jīng)常見(jiàn)到,是調(diào)用的意思,因此invokevirtual #3代表的就是 調(diào)用常量池索引為3的字面量對(duì)應(yīng)的方法,這里的對(duì)應(yīng)方法就是java/io/PrintStream.println,

最終,將變量b的值打印出來(lái)。

4.3.2 ++i過(guò)程

再來(lái)看看++b操作:

//Source code
b = ++b;

//Byte code
14 iinc 1 by 1
17 iload_1
18 istore_1

這里的三行字節(jié)碼與前面講解的b=b++中的字節(jié)碼完全一樣,只是順序發(fā)生了變化:

先在局部變量表中自增(iinc 1 by 1),然后再入棧到操作數(shù)棧中(iload_1),最后出棧保存到局部變量表中(istore_1)。

先自增就保證了自增操作是有效的,不管后面怎么折騰,參與的都是已經(jīng)自增后的值,來(lái)個(gè)動(dòng)圖:

4.4 靜態(tài)變量自增

最后我們看看靜態(tài)變量a的自增操作:


//Source code
a = a++;

//Byte code
26 getstatic #4 <com/cc/demo/Hello.a : I>
29 dup
30 iconst_1
31 iadd
32 putstatic #4 <com/cc/demo/Hello.a : I>
35 putstatic #4 <com/cc/demo/Hello.a : I>

getstatic #4?就是獲取常量池中索引為#4的字面量對(duì)應(yīng)的靜態(tài)字段。前面已經(jīng)講過(guò)了,這一步是到堆中去拿的,拿到靜態(tài)變量的值以后,會(huì)放到當(dāng)前棧幀的操作數(shù)棧。

然后執(zhí)行dup操作,dup是duplicate的縮寫(xiě),意思是復(fù)制。

dup指令的意義就是復(fù)制頂部操作數(shù)堆棧值并壓入棧中,也就是說(shuō)此時(shí)的棧頂有兩個(gè)一模一樣的元素。

這是個(gè)什么操作啊,兩份一樣的值能干什么,別急,我們繼續(xù)往下看。

隨后是一個(gè)iconst_1,將int類型的數(shù)值1壓入棧頂。

然后是一個(gè)iadd指令,這個(gè)指令是將操作數(shù)棧棧頂?shù)膬蓚€(gè)int類型元素彈出并進(jìn)行加法運(yùn)算,最后將求得的結(jié)果壓入棧中。

像這種兩個(gè)值進(jìn)行數(shù)值運(yùn)算的操作,其實(shí)是操作數(shù)棧中除了簡(jiǎn)單的入棧出棧外最常見(jiàn)的操作了。

類似的還有isub——棧頂兩個(gè)值相減后結(jié)果入棧,imul——棧頂兩個(gè)值相乘后結(jié)果入棧等等。

總之,此時(shí)的棧頂最上面的兩個(gè)元素是剛剛壓入棧的常量1以及靜態(tài)變量a的值0(這是剛才dup之后壓入棧的那個(gè)),這兩數(shù)一加,結(jié)果入棧,那還是個(gè)1。

接下來(lái)的指令是一個(gè) putstatic #4,取棧頂元素出棧并賦值給靜態(tài)變量,這里當(dāng)然就是靜態(tài)變量a啦。

因此靜態(tài)變量a的值就自增完成,變成了1。

可是!!!

事情到這里還沒(méi)結(jié)束,因?yàn)樽止?jié)碼中清清楚楚地記錄著隨后又進(jìn)行了一次 putstatic #4操作。

此時(shí)的棧頂元素就是最開(kāi)始從堆中取過(guò)來(lái)的變量a的初始值0,現(xiàn)在把這個(gè)值出棧,又賦值給了a,這不是中間的操作都白費(fèi)了嗎?

靜態(tài)變量a的值又變成0了。

等等,這一波脫褲子放屁的操作怎么似曾相識(shí)?

前面局部變量b = b++好像也經(jīng)歷過(guò)這么一個(gè)過(guò)程,先復(fù)制一份自己到操作數(shù)棧中,然后局部變量表里的值一頓操作,最后操作數(shù)棧中的原始值又跑回去把自己給覆蓋了。

靜態(tài)變量不遠(yuǎn)萬(wàn)里從堆中趕到操作數(shù)棧,先復(fù)制一份自己造了個(gè)分身到操作數(shù)棧棧頂,隨后對(duì)這個(gè)棧頂?shù)姆稚硪活D操作,最后留在操作數(shù)棧中的原始值又跑回去把自己給覆蓋了。

難道說(shuō),這波復(fù)制操作是因?yàn)殪o態(tài)變量需要分配一個(gè)位置充當(dāng)局部變量表的作用,另一個(gè)位置需要充當(dāng)操作數(shù)棧位置的作用?

為了驗(yàn)證這個(gè)猜測(cè)是否正確,我們最后來(lái)看看a = ++a:

//Source code
a = ++a;

//Byte code
47 getstatic #4 <com/cc/demo/Hello.a : I>
50 iconst_1
51 iadd
52 dup
53 putstatic #4 <com/cc/demo/Hello.a : I>
56 putstatic #4 <com/cc/demo/Hello.a : I>

相信大家閱讀這一段字節(jié)碼已經(jīng)沒(méi)有問(wèn)題了,我只講講中間幾句最重要的:

靜態(tài)變量a從堆中被復(fù)制到操作數(shù)棧之后,緊跟的是一個(gè)iconst_1,將int類型的數(shù)值1壓入棧頂。

然后是一個(gè)iadd指令,將操作數(shù)棧棧頂?shù)膬蓚€(gè)int類型元素彈出并進(jìn)行加法運(yùn)算,也就是剛剛壓入棧的常量1以及靜態(tài)變量a的值0進(jìn)行求和操作。

這兩數(shù)一加,結(jié)果入棧,那就是個(gè)1。

接下來(lái)有意思了,進(jìn)行了一次dup操作,那操作數(shù)棧中的棧頂此時(shí)就有兩個(gè)1了。

這跟執(zhí)行++b時(shí),局部變量先在局部變量表中自增,再?gòu)?fù)制一份到操作數(shù)棧的操作是不是很像?

然后是兩個(gè) putstatic #4,取棧頂元素出棧并賦值給靜態(tài)變量,現(xiàn)在棧頂兩個(gè)都是1,即使賦值兩次,最終靜態(tài)變量a的值還得是1啦。

懂了嗎寶,一切的源頭就是因?yàn)殪o態(tài)變量被加載到棧幀后不能加入局部變量表,因此它將自己的一個(gè)分身壓到棧頂,現(xiàn)在操作數(shù)棧中有兩個(gè)一模一樣的值,一個(gè)充當(dāng)局部變量表的作用,另一個(gè)充當(dāng)正常操作數(shù)棧位置的作用。

5.小結(jié)

俗話說(shuō),授人以魚(yú)不如授人以漁。本文通過(guò)對(duì)虛擬機(jī)結(jié)構(gòu)的簡(jiǎn)單介紹,慢慢引申到字節(jié)碼的執(zhí)行的過(guò)程。

最后用兩個(gè)例子一步一步手撕字節(jié)碼,跟著這個(gè)思路思考,相信大家以后遇到字節(jié)碼的問(wèn)題也能稍微有點(diǎn)頭緒了吧。

責(zé)任編輯:武曉燕 來(lái)源: 敖丙
相關(guān)推薦

2024-09-19 08:51:01

HTTP解密截取

2022-01-05 21:54:51

網(wǎng)絡(luò)分層系統(tǒng)

2022-08-13 12:07:14

URLHTTP加密

2024-11-26 08:52:34

SQL優(yōu)化Kafka

2022-10-10 08:13:16

遞歸通用代碼

2022-05-10 22:00:41

UDPTCP協(xié)議

2022-06-01 11:52:42

網(wǎng)站客戶端網(wǎng)絡(luò)

2022-08-18 17:44:25

HTTPS協(xié)議漏洞

2024-11-11 10:34:55

2022-10-19 14:08:42

SYNTCP報(bào)文

2022-11-30 17:13:05

MySQLDynamic存儲(chǔ)

2022-01-11 20:43:16

TCPIP模型

2024-09-04 15:17:23

2022-12-02 13:49:41

2024-10-31 08:50:14

2022-09-05 14:36:26

服務(wù)端TCP連接

2022-07-26 00:00:02

TCPUDPMAC

2022-12-13 18:09:25

連接狀態(tài)客戶端

2024-10-15 10:59:18

Spring MVCJava開(kāi)發(fā)

2024-03-18 08:21:06

TCPUDP協(xié)議
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)