溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

JVM虛擬機棧——JAVA方法的消亡史

發(fā)布時間:2020-07-03 13:57:45 來源:網(wǎng)絡(luò) 閱讀:678 作者:沙漏半杯 欄目:編程語言

引子

這是由一個“無聊”的問題引發(fā)的故事:方法ipp和ppi分別會打印什么結(jié)果?

public class Opcode {	public static void main(String[] args) {
		System.out.println("hello wang ni ma");
	}	public void ipp(){		int i = 0;
		i = i++;
		System.out.println(i);
	}	public void ppi(){		int i = 0;
		i = ++i;
		System.out.println(i);
	}
}

當(dāng)然了,把兩個方法放在一起,憑借些許的邏輯思維分析,可以很快給出答案: 0 1

那JVM為什么會執(zhí)行出這樣的結(jié)果呢,本文將結(jié)合 字節(jié)碼 和 虛擬機棧 做出解釋。

番外

javap 反匯編器

    javap是JDK自帶的反匯編器,可以查看java編譯器為我們生成的字節(jié)碼。通過它,我們可以對照源代碼和字節(jié)碼,從而了解很多編譯器內(nèi)部的工作。

java字節(jié)碼指令集

    Java 程序編譯之后就變成了一條條字節(jié)碼指令,其形式類似匯編,但和匯編有不同之處:

  •     匯編指令的操作數(shù)存放在數(shù)據(jù)段和寄存器中,可通過存儲器或寄存器尋址找到需要的操作數(shù);

  •     Java 字節(jié)碼指令的操作數(shù)存放在操作數(shù)棧中(可以理解為JVM內(nèi)部虛擬寄存器),當(dāng)執(zhí)行某條帶 n 個操作數(shù)的指令時,就從棧頂取 n 個操作數(shù),然后把指令的計算結(jié)果(如果有的話)入棧。

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

 

    Java虛擬機的指令由一個字節(jié)長度的、代表著某種特定操作含義的操作碼(Opcode)以及跟隨其后的零至多個代表此操作所需參數(shù)的操作數(shù)(Operands)所構(gòu)成。

    列舉本文用到的基本指令:

  • 加載和存儲指令

        將一個局部變量加載到操作棧:iload_<n>

        將一個數(shù)值從操作數(shù)棧存儲到局部變量表:istore_<n>

        將一個常量加載到操作數(shù)棧:iconst_<i>

  • 運算指令 (運算之后的結(jié)果會自動入棧) 

        局部變量自增指令:iinc

    對于大部分與數(shù)據(jù)類型相關(guān)的字節(jié)碼指令,他們的操作碼助記符中都有特殊的字符來表明專門為哪種數(shù)據(jù)類型服務(wù):

  • i -- int

  • l -- long

  • s -- short

  • b -- byte

  • c -- char

  • f -- float

  • d -- double

  • a -- reference

 正題

    我們用javac編譯上面的Opcode.java,然后“javap -c”查看字節(jié)碼:

    JVM虛擬機棧——JAVA方法的消亡史

    javap命令加入“-v”可以看到更詳細的信息(常量池) :

    JVM虛擬機?!狫AVA方法的消亡史

虛擬機棧圖解

在看圖之前我們先了解幾個概念:

  • Java虛擬機(JVM)是基于棧結(jié)構(gòu)的,其中的“?!敝傅木褪遣僮鲾?shù)棧。

  • 在代碼的實際運行中,每個線程都會創(chuàng)建一個JVM棧存儲棧幀(frame)。每當(dāng)有方法調(diào)用時,frame就會被創(chuàng)建;當(dāng)這個方法返回時,frame出棧。

  • 一個frame由三部分組成:操作數(shù)棧(Oprand Stack)、局部變量表(Local Variable Table)、當(dāng)前方法所在類的運行常量池的引用(The Reference of Constant Pool)。

    • 局部變量表,存儲的是方法的參數(shù)和局部變量的值。存儲參數(shù)的索引從0開始,如果是構(gòu)造方法或者實例化方法的frame,那么局部變量數(shù)組的“0”處存儲的是“this”引用,然后再從“1”開始存儲形參和局部變量;如果是靜態(tài)方法的frame,局部變量表不會存儲“this”引用,而是從“0”開始存儲形參和局部變量。

    • 操作數(shù)棧,臨時存儲參與運算的數(shù)值,然后進行相關(guān)操作。和局部變量表一樣,操作數(shù)棧也是一個以字長為單位的數(shù)組。但是和前者不同的是,它不是通過索引來訪問,而是通過標(biāo)準的棧操作壓棧/出棧來訪問的。

    • 常量池,存儲在JVM內(nèi)存線程共享區(qū)的“方法區(qū)”,在類初始化的時候,會為給出的常量分配一個常量池,并且為每一個常量給出引用。

ipp()

    把常量“0”加載到操作數(shù)棧,指令“iconst_0”中的“0”代表int常量“0”

        操作數(shù)棧:[0]

        局部變量表:[this]

JVM虛擬機?!狫AVA方法的消亡史

 

“i = i++;”進行了四次操作:

    1 “istore_1”將操作數(shù)棧中棧頂?shù)膇nt壓入局部變量表“1”的位置

        操作數(shù)棧:[]

        局部變量表:[this, 0]

    2 “iload_1”將局部變量表“1”處的int加載到操作數(shù)棧

        操作數(shù)棧:[0]

        局部變量表:[this, 0]

    3 “iinc  1, 1”將局部變量表“1”處的int做自增運算,結(jié)果自動入棧

        操作數(shù)棧:[0]

        局部變量表:[this, 1]

    4 “istore_1”將操作數(shù)?!?”處的int壓入局部變量表

        操作數(shù)棧:[]

        局部變量表:[this, 0]

JVM虛擬機棧——JAVA方法的消亡史

 

“System.out.println(i);”進行了三次操作:

    1 “getstatic    #2”指向常量池中的第2個位置,載入“System.out”域

    2 “iload_1”將局部變量表“1”處的int加載到操作數(shù)棧

        操作數(shù)棧:[0]

        局部變量表:[this, 0]

    3 “invokevirtul    #5”指向常量池中的第5個位置,”調(diào)用實例方法“println”打印操作數(shù)棧的數(shù)值“0”

JVM虛擬機?!狫AVA方法的消亡史

至此,ipp()方法的分析完成了,理解之后,反觀ppi()方法的字節(jié)碼信息,有一處不同:

JVM虛擬機棧——JAVA方法的消亡史

    相當(dāng)于“i = i++”操作是先加載了局部變量表中的“0”到操作數(shù)棧,然后在局部變量表中做自增運算;而“i = ++i”是先在局部變量表中做自增運算,此時的值已經(jīng)變成“1”,然后再把局部變量表中的“1”加載到操作數(shù)棧。這也就印證了坊間流傳的“i++是先賦值后運算,++i是先運算后賦值”這一說法。

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI