本文系作者投稿,原作者:程序員DMZ,原文地址:https://blog.csdn.net/qq_41907991/article/details/105337049
面試官:“說一說i++跟++i的區別”
我:“i++是先把i的值拿出來使用,然後再對i+1,++i是先對i+1,然後再去使用i”
面試官:“那你看看下面這段代碼,運行結果是什麼?”
public static void main(String[] args) {
int j = 0;
for (int i = 0; i < 10; i++) {
j = (j++);
}
System.out.println(j);
}
“以我多年的開發經驗來看,它必然不會是10”
面試官:
我:“哈哈.....,開個玩笑,結果爲0啦”
面試官:“你能說說爲什麼嗎?”
我:“因爲j++這個表達式每次返回的都是0,所以最終結果就是0”
面試官:“小夥子不錯,那你能從JVM的角度講一講爲什麼嘛?”
我心想:這貨明顯是在搞事情啊,這麼快就到JVM了?還好我有準備。
首先我們知道,JVM的運行時數據區域是分爲好幾塊的,具體分佈如下圖所示:現在我們主要關注其中的虛擬機棧,關於虛擬機棧,我們知道它有以下幾個特點:
Java虛擬機棧是線程私有的,它的生命週期和線程相同
Java虛擬機棧是由一個個棧幀組成,線程在執行一個方法時,便會向棧中放入一個棧幀。
每一個方法所對應的棧幀又包含了以下幾個部分
局部變量表
操作數棧
方法出口
.........
那麼現在虛擬機棧就可以表示成下面這個樣子:
其中的局部變量表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。
“局部變量表的最小存儲單元爲Slot(槽),其中64位長度的long和double類型的數據會佔用2個Slot,其餘的數據類型只佔用1個。所以我們可以將局部變量表分爲一個個的存儲單元,每個存儲單元有自己的下標位置,在對數據進行訪問時可以直接通過下標來訪問
操作數棧對於數據的存儲跟局部變量表是一樣的,但是跟局部變量表不同的是,操作數棧對於數據的訪問不是通過下標而是通過標準的棧操作來進行的(壓入與彈出),之後在分析字節碼指令時我們會很明顯的感覺到這一點。另外還有,對於數據的計算是由CPU完成的,所以CPU在執行指令時每次會從操作數棧中彈出所需的操作數經過計算後再壓入到操作數棧頂。
以執行下面這段代碼爲例:
public static void main(String[] args){
int a = 2;
int b = 3;
int c = a + b;
}
這個過程如下所示
這兩步完成了局部變量a的賦值,同理b的賦值也一樣,a,b完成賦值後此時的狀態如下圖所示
此時要執行a+b的運算了,所以首先要將需要的操作數加載到操作數棧,執行運算時再將操作數從棧中彈出,由CPU完成計算後再將結果壓入到棧中,整個過程如下:
到這裏還沒有完哦,還剩最後一步,需要將計算後的結果賦值給c,也就是要將操作數棧的數據彈出並賦值給局部變量表中的第三個槽位
OK,到這一步整個過程就完成了
面試官:“嗯,說的不錯,但是你還是沒解釋爲什麼最開始的那個問題,爲什麼j=j++
的結果會是0呢?”
我:“面試官您好,要解釋這個問題上面的知識都是基礎,真正要說明白這個問題我們需要從字節碼入手。”
我們進入到這段代碼編譯好的.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行字節碼指令,用圖表示如下:
可以看到本來局部變量表中的j已經完成了自增(iinc
指令是直接對局部變量進行自增),但是在進行賦值時是將操作數棧中的數據彈出,但是操作數棧的數據並沒有經過計算,所以每次自增的結果都被覆蓋了。最終結果就是0。
我們平常說的i++是先拿去用,然後再自增,而++i是先自增再拿去用。這個到底怎麼理解呢?如果站在JVM的層次來講的話,應該這樣說:
i++是先被操作數棧拿去用了(先執行的load指令),然後再在局部變量表中完成了自增,但是操作數棧中還是自增前的值
而++1是先在局部變量表中完成了自增(先執行innc指令),然後再被load進了操作數棧,所以操作數棧中保存的是自增後的值
這就是它們的根本區別。
最後我這裏放出一段代碼及其字節碼,我相信看完這篇文章你對於i++及++i的理解絕對跟原來不一樣了
public static void main(String[] args) {
int i = 4;
int b = i++;
int a = ++i;
}
public static void main(java.lang.String[]);
Code:
0: iconst_4
1: istore_1
2: iload_1
3: iinc 1, 1
6: istore_2
7: iinc 1, 1
10: iload_1
11: istore_3
12: return
這段代碼大家自行思考,有任何問題可以給我留言哦~
碼字不易,記得點個贊哈~
PS:圖中局部變量表的下標都是從1開始,這是因爲我直接用main函數測試的,局部變量表中下標爲0的元素是main函數中的形參,也就是String[]args。另外也通過這些過程我們也可以發現,局部變量表就是通過下標訪問的,而操作數棧就是通過正常的棧操作(壓入/彈出)來完成數據訪問的
有道無術,術可成;有術無道,止於術歡迎大家關注Java之道公衆號
好文章,我在看❤️