你說你懂i++和++i的區別,能從JVM的解析解釋一下嗎?

起因

面試官:你說你懂i++跟++i的區別,那你知道下面這段程式碼的執行結果嗎?

面試官:“說一說i++跟++i的區別”

我:“i++是先把i的值拿出來使用,然後再對i+1,++i是先對i+1,然後再去使用i”

面試官:“那你看看下面這段程式碼,執行結果是什麼?”

你說你懂i++和++i的區別,能從JVM的解析解釋一下嗎?

“以我多年的開發經驗來看,它必然不會是10”

面試官:

你說你懂i++和++i的區別,能從JVM的解析解釋一下嗎?

我:“哈哈…,開個玩笑,結果為0啦”

面試官:“你能說說為什麼嗎?”

我:“因為j++這個表示式每次返回的都是0,所以最終結果就是0”

面試官:“小夥子不錯,那你能從JVM的角度講一講為什麼嘛?”

我心想:這貨明顯是在搞事情啊,這麼快就到JVM了?還好我有準備。

JVM解釋

首先我們知道,JVM的執行時資料區域是分為好幾塊的,具體分佈如下圖所示:

在這裡插入圖片描述

你說你懂i++和++i的區別,能從JVM的解析解釋一下嗎?

現在我們主要關注其中的虛擬機器棧,關於虛擬機器棧,我們知道它有以下幾個特點:

Java虛擬機器棧是執行緒私有的,它的生命週期和執行緒相同

Java虛擬機器棧是由一個個棧幀組成,執行緒在執行一個方法時,便會向棧中放入一個棧幀。

每一個方法所對應的棧幀又包含了以下幾個部分

區域性變量表

運算元棧

方法出口

那麼現在虛擬機器棧就可以表示成下面這個樣子:

你說你懂i++和++i的區別,能從JVM的解析解釋一下嗎?

其中的區域性變量表存放了編譯期可知的各種基本資料型別(boolean、byte、char、short、int、float、long、double)、物件引用。區域性變量表所需的記憶體空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的區域性變數空間是完全確定的,在方法執行期間不會改變區域性變量表的大小。

區域性變量表的最小儲存單元為Slot(槽),其中64位長度的long和double型別的資料會佔用2個Slot,其餘的資料型別只佔用1個。所以我們可以將區域性變量表分為一個個的儲存單元,每個儲存單元有自己的下標位置,在對資料進行訪問時可以直接透過下標來訪問

運算元棧對於資料的儲存跟區域性變量表是一樣的,但是跟區域性變量表不同的是,運算元棧對於資料的訪問不是透過下標而是透過標準的棧操作來進行的(壓入與彈出),之後在分析位元組碼指令時我們會很明顯的感覺到這一點。另外還有,對於資料的計算是由CPU完成的,所以CPU在執行指令時每次會從運算元棧中彈出所需的運算元經過計算後再壓入到運算元棧頂。

以執行下面這段程式碼為例:

public static void mian(String[] args){ int a = 2; int b = 3; int c = a + b;}

這個過程如下所示

你說你懂i++和++i的區別,能從JVM的解析解釋一下嗎?

這兩步完成了區域性變數a的賦值,同理b的賦值也一樣,a,b完成賦值後此時的狀態如下圖所示

你說你懂i++和++i的區別,能從JVM的解析解釋一下嗎?

此時要執行a+b的運算了,所以首先要將需要的運算元載入到運算元棧,執行運算時再將運算元從棧中彈出,由CPU完成計算後再將結果壓入到棧中,整個過程如下:

你說你懂i++和++i的區別,能從JVM的解析解釋一下嗎?

到這裡還沒有完哦,還剩最後一步,需要將計算後的結果賦值給c,也就是要將運算元棧的資料彈出並賦值給區域性變量表中的第三個槽位

你說你懂i++和++i的區別,能從JVM的解析解釋一下嗎?

OK,到這一步整個過程就完成了

面試官:“嗯,說的不錯,但是你還是沒解釋為什麼最開始的那個問題,為什麼j=j++的結果會是10呢?”

我:“面試官您好,要解釋這個問題上面的知識都是基礎,真正要說明白這個問題我們需要從位元組碼入手。”

我們進入到這段程式碼編譯好的。class檔案目錄下執行:javap -c xxx。class,得到其位元組碼如下:

// 為方便閱讀將對應程式碼也放到這裡

public static void main(String[] args) { int j = 0; for (int i = 0; i < 10; i++) { j = (j++); } System。out。println(j);}

public static void main(java。lang。String[]);

Code:

0: iconst_0 // 將常數0壓入到運算元棧頂

1: istore_1 // 將運算元棧頂元素彈出並壓入到區域性變量表中1號槽位,也就是j=0

2: iconst_0 // 將常數0壓入到運算元棧頂

3: istore_2 // 將運算元棧頂元素彈出並壓入到區域性變量表中2號槽位,也就是i=0

4: iload_2 // 將2號槽位的元素壓入運算元棧頂

5: bipush 10 // 將常數10壓入到運算元棧頂,此時運算元棧中有兩個數(常數10,以及i)

7: if_icmpge 21 // 比較運算元棧中的兩個數,如果i<10,跳轉到第21行

10: iload_1 // 將區域性變量表中的1號槽位的元素壓入到運算元棧頂,就是將j=0壓入運算元棧頂

11: iinc 1, 1 // 將區域性變量表中的1號元素自增1,此時區域性變量表中的j=1

14: istore_1 // 將運算元棧頂的元素(此時棧頂元素為0)彈出並賦值給區域性變量表中的1號 槽位(一號槽位本來已經完成自增了,但是又被賦值成了0)

15: iinc 2, 1 // 將區域性變量表中的2號槽位的元素自增1,此時區域性變量表中的2號元素值為1,也就是i=1

18: goto 4 // 第一次迴圈結束,跳轉到第四行繼續迴圈

21: getstatic #2 // Field java/lang/System。out:Ljava/io/PrintStream;

24: iload_1

25: invokevirtual #3 // Method java/io/PrintStream。println:(I)V

28: return

我們著重關注第10,11,14行位元組碼指令,用圖表示如下:

你說你懂i++和++i的區別,能從JVM的解析解釋一下嗎?

可以看到本來區域性變量表中的j已經完成了自增(iinc指令是直接對區域性變數進行自增),但是在進行賦值時是將運算元棧中的資料彈出,但是運算元棧的資料並沒有經過計算,所以每次自增的結果都被覆蓋了。最終結果就是0。

我們平常說的i++是先拿去用,然後再自增,而++i是先自增再拿去用。這個到底怎麼理解呢?如果站在JVM的層次來講的話,應該這樣說:

i++是先被運算元棧拿去用了(先執行的load指令),然後再在區域性變量表中完成了自增,但是運算元棧中還是自增前的值

而++1是先在區域性變量表中完成了自增(先執行innc指令),然後再被load進了運算元棧,所以運算元棧中儲存的是自增後的值

這就是它們的根本區別。

最後我這裡放出一段程式碼及其位元組碼,我相信看完這篇文章你對於i++及++i的理解絕對跟原來不一樣了

你說你懂i++和++i的區別,能從JVM的解析解釋一下嗎?

原文連結:https://blog。csdn。net/qq_41907991/article/details/105337049