jvm是如何執行i = i++ + ++i的,你知道嗎?

liuxuhui發表於2021-09-09

結果是多少?

public static void main(String[] args) {    int i = 0;
    i = i++ + ++i;
    System.out.println(i);
}//結果輸出 2

為什麼是2?

一個.java檔案首先要被編譯成.class檔案jvm才能夠執行,而jvm是根據java程式碼生成的位元組碼來確認他要如何執行程式的。說的再通俗一點就是,jvm看不懂java程式碼,他能看懂的是位元組碼,而編譯就是這麼一個翻譯的過程。
  所以為了瞭解i = i++ + ++i的執行原理,我們首先反彙編這段程式碼(請先編譯java檔案,Main.java是我的檔名):在命令列下輸入
javap -c Main.class
可以看到位元組碼是:

       0: iconst_0
       1: istore_1
       2: iload_1
       3: iinc          1, 1
       6: iinc          1, 1
       9: iload_1
      10: iadd
      11: istore_1
      12: getstatic     #2      // Field java/lang/System.out:Ljava/io/PrintStream;
      15: iload_1
      16: invokevirtual #3      // Method java/io/PrintStream.println:(I)V
      19: return

圖片描述


  不要怕,這其實很容易。為了不至於引入太多複雜概念,這裡只需要知道程式在執行時的兩個區域,一個叫做區域性變數表(Local Variable),一個叫做運算元棧(Operand Stack),前者的結構類似陣列,用來儲存區域性變數,後者的資料結構是棧,用來輔助執行指令。
  我一條條解釋上述指令。先看下圖,因為這裡只用到了1個區域性變數表的位置,所以其他的就沒寫出來。


圖片描述


  紅框的內容代表每條指令執行完,這兩個區域的值是多少。建議認真看下每一行的值是怎麼來的再往下看。實際上在執行到getstatic #2這條命令的時候,我們的想知道的問題已經計算完了。主要關注這條命令以上的命令即可。並且我們的值最終也儲存在區域性變數表1號位置。
  所以最終輸出2是理所當然的。


規律是什麼?

既然說class檔案是java檔案的“翻譯”過來的,那麼java程式碼和位元組碼總有對應關係吧?我們試著找一下。
  我們說區域性變數表是用來存放區域性變數的,第二條指令又向區域性變數表中存入值了,根據指令的解釋很容易能夠猜到前兩條指令iconst_0 istore_1對應java程式碼int i = 0;
  我們透過最後的輸出知道了i的值是存在區域性變數表1中的,那麼istore_1這個向區域性變數表1號位置賦值的語句一定就是將前面計算得到的i++ + ++i的結果存進表的意思,也就是意味著在執行istore_1語句時,運算元棧棧頂的元素就是我們計算的結果。
  繼續往上推,iadd指令執行的時候,棧頂的兩個元素一定一個就是i++ 另外一個就是++i的值。除去最開始的兩條指令,一共只剩下四條指令了分別是

  iload_1
  iinc          1, 1
  iinc          1, 1
  iload_1

猜也能猜出來前兩個對應一條指令,後兩個對應一條,畢竟這麼兩個相似的指令不可能翻譯出來位元組碼的命令數還不相等吧。問題是前兩個和後兩個誰對應i++誰對應++i。我們先回憶一下這兩條語句在java上有什麼不同,簡單說“i++是先用再加,++i是先加再用”。另外需要再講一個東西,我前面說運算元棧是用來輔助執行命令的,形象點理解就是運算元棧裡面的東西是馬上就要拿來用的,而區域性變數表是用來暫時先儲存下變數的。好了,回到我們剛才的問題,再想一下,你應該就能夠想到:前兩條指令對應i++而後兩條對應++i。前兩條位元組碼的含義是:我準備用1號變數,先放在棧裡(先用),好了,我已經放在棧裡了,你在區域性變數表裡可以加1了(再加)。後兩條位元組碼的含義是:你在區域性變數表裡先加1(先加),然後我要放在棧裡了(再用)。
  這樣我們就把每條語句及其對應的位元組碼都找出來了,那麼規律到底是什麼?
  我們現在用更加通俗的話來解釋i= i++ + ++i。首先這個語句等價於i = (i++) + (++i)。執行順序是:

  1. 計算i++

  2. 計算++i

  3. 將前兩個計算的結果加起來賦值給i

看起來好像在說廢話,那麼我們結合之前的位元組碼來分析。

  1. 步驟1還可以分成2步

    1. 將當前i的值,複製一份(假如複製出來的元素叫copy1 )。翻譯成程式碼:int copy1 = i; (最開始i為0)

    2. 將i的值加1。翻譯成程式碼:i++;(此時i為1)

  2. 步驟2同樣分成2步

  3. 將i的值加1。翻譯成程式碼:i++;(此時i為2)

  4. 將當前i的值,複製一份(假如複製出來的元素叫copy2 )。翻譯成程式碼:int copy2 = i;(此時i還是2)

  5. 將兩個計算結果加起來。i= copy1 + copy2      (也就是0+2)

總結起來:i= i++ + ++i真實執行過程的虛擬碼就是

int copy1 = i;
i++

i++int copy2 = i

i = copy1 + copy2;

那麼我們現在來試著算一下 i = ++i + i++ + ++i + ++i (i的初始值是0)

首先我們知道,i = ++i + i++ + ++i + ++i 等價於 i = (++i) + (i++) + (++i) + (++i)。
我們將四個括號裡的值分別起名為r1、r2、r3、r4。表示式從左向右計算。

  1. 首先計算r1:++i要先將i加1,然後賦值給r1,所以r1等於1。(執行完這條語句時i的值為1)

  2. 然後計算r2:i++要先將i的值賦值給r2,然後i字加1,所以r2等於1。(執行完這條語句時i的值為2)

  3. 然後計算r3:++i要先將i加1,然後賦值給r3,所以r3等於3。(執行完這條語句時i的值為3)

  4. 然後計算r4:++i要先將i加1,然後賦值給r4,所以r3等於4。(執行完這條語句時i的值為4)

  5. 然後計算r1+r2+r3+r4,等於1+1+3+4,結果為9

  6. 最後將9賦值給i。(其實此時i是有值的,就是之前的4,但是被剛賦值進來的9給覆蓋了,所以就沒能表現出來)

怎麼樣,你算出來了嗎?

這篇文章是最近學習jvm以來想通的一個以前一直不明白的問題,在此與大家分享下。如果有不懂的,可以下面留言。如有錯誤也請指出。

作者:Nancy945
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/756/viewspace-2809492/,如需轉載,請註明出處,否則將追究法律責任。

相關文章