為何強大 記錄全面: 包含請求路徑、請求方法、客戶端IP、設備標識、荷載數據、文件上傳、請求頭、業務邏輯處理時間、業務邏輯所耗記憶體、用戶id、以及響應數據。 配置簡單: 預設不需要寫任何邏輯可開箱即用,靠前4個方法,就可指定某些url不記錄日誌,或不記錄某些請求頭,不記錄某些荷載數據,或決定是否返回 ...
前言:眾所周知,
i++
和++i
的區別是:i++
先將i
的值賦值給變數,再將i
的值自增1;而++i
則是先將i
的值自增1,再將結果賦值給變數。因此,二者最終都給i
自增了1,只是方式不同而已。當然,如果在面試過程中面試官問你這個問題,只回答出上述內容,只能說明你對這方面的知識瞭解的還是太淺顯。那麼
i++
和++i
到底有什麼不同之處呢?
一、局部變數表與操作數棧簡介
《深入理解Java虛擬機》第八章對棧幀結構有如下描述Java虛擬機以方法作為最基本的執行單元,“棧幀”(Stack Frame)則是用於支持虛擬機進行方法調用和方法執行背後的數據結構,它也是虛擬機運行時數據區中的虛擬機棧的棧元素。
在一個活動線程中,可能會執行多個方法,因此會存在多個棧幀,和“棧”(先進後出)一樣,處於棧頂的棧幀才是真正運行的,處於棧頂的棧幀稱作“當前棧幀”(Current Stack Frame),這個棧幀所屬的方法稱作“當前方法”(Current Method)。
在執行main
方法時,main
方法所屬的線程主線程,假設在主線程中調用了一個method1()
方法,在method1()
內部調用了method2()
方法,在method2()
方法執行兩個整數運算,示例如下:
/**
* 方法調用
*
* @author iCode504
* @date 2023-10-23 22:05
*/
public class StackFrameDemo1 {
public static void main(String[] args) {
System.out.println("main開始執行");
method1();
System.out.println("main執行完成");
}
private static void method1() {
System.out.println("method1開始執行");
int result = method2();
System.out.println("result = " + result);
System.out.println("method1執行結束");
}
private static int method2() {
int var1 = 10;
int var2 = 20;
return var1 + var2;
}
}
運行結果:
由代碼我們可以看出,main
方法最先執行一個輸出,然後進入method1
執行第一個輸出,再完整執行method2
。method2
執行完成以後,再執行method1
,最後執行main
方法,由於這段代碼中只涉及一個主線程,並且最先完整執行方法的是method2
,因此method2
對應的棧幀就是當前棧幀,main
方法最後執行完畢,因此main
方法對應的棧幀在method2
和method1
之下。以下是這段代碼對應的棧幀概念圖:
在每一個棧幀中存儲了方法的局部變數表、操作數棧、動態鏈接和方法返回地址等信息
1.1 局部變數表
局部變數表(Local variable Table)是一組變數值的存儲空間,用於存放方法參數和方法內部定義的局部變數。
局部變數表的容量是以變數槽(Variable Slot)為最小單位,每個變數槽能存儲基本數據類型和引用數據類型的數據。為了儘可能節省棧幀消耗的記憶體空間,局部變數表中的變數槽是可以重用的。
JVM使用索引定位的方式使用索引變數表,索引值的範圍是從0開始到局部變數表最大變數槽的數量(類似數組結構)。
當一個方法被調用的時候,JVM會使用局部變數表來完成參數值到參數變數列表的傳遞,即實參到形參的傳遞。
1.2 操作數棧
操作數棧(Operand Stack)也稱作操作數棧,它是一個棧結構(後進先出,例如手槍的彈夾,先打出去的子彈是最頂上的子彈)。
在方法開始執行的時候,這個方法對應的操作數棧是空的,在方法執行過程中,會有各種位元組碼指令向操作數棧中寫入或讀取內容,即出棧和入棧操作,例如:兩數相加運算時,就需要將兩個數壓入棧頂後調用運算指令。
操作數棧中的元素的數據類型必須和位元組碼指令序列嚴格匹配,在編譯程式代碼的時候編譯器必須要嚴格保證這一點,在類的校驗階段的數據流分析時候還需要再次校驗。例如:執行加法iadd
(i
是int
類型,add
是兩個數相加)命令時,就需要保證兩個操作數必須是int
類型,不能出現其他類型相加的情況。
二、位元組碼分析(圖解)
我們可以從位元組碼的角度進一步對i++
和++i
的執行過程做進一步的分析。以下麵代碼為例:
/**
* i++和++i的深入分析
*
* @author iCode504
* @date 2023-10-17 5:58
*/
public class IncrementAndDecrementOperators2 {
public static void main(String[] args) {
int intValue1 = 2;
int intValue2 = 2;
int result1 = intValue1++;
int result2 = ++intValue2;
System.out.println("result1 = " + result1);
System.out.println("result2 = " + result2);
}
}
我們需要查看編譯後的位元組碼文件,位元組碼文件不能直接使用記事本打開,但是我們可以使用javap -verbose 文件名.class
命令,以IncrementAndDecrementOperators2.class
為例:
javap -verbose IncrementAndDecrementOperators2.class
此時就會打開所有的位元組碼文件,我們只需要關註main
方法內的執行過程即可:
首先來解釋一下這四行代碼的含義:
0: iconst_2
1: istore_1
2: iconst_2
3: istore_2
iconst_2
一共有兩部分組成,i
指的是int
類型(源代碼中我們定義的確實是int
類型),const
代表常量(數字2
是整型常量),iconst_2
的含義是將2入操作數棧。istore_1
中的store
代表的是存儲,istore_1
的含義是將操作數棧中的數值2出棧,存入到局部變數表1的位置。同理,i_store2
表示將操作數棧中的數值2出棧,存儲到局部變數表2的位置。
以下是前面四行代碼存儲過程圖(存儲過程全部流程圖點擊此鏈接下載:點我下載):
此時我們繼續觀察4-8行代碼:
4: iload_1
5: iinc 1, 1
8: istore_3
iload_1
的作用是將局部變數表1號位置存儲的值移動到操作數棧的棧頂。- 第5行的
iinc
有兩個參數,第一個參數1
是局部變數表的位置,另一個參數1
的含義是在該位置存儲一個1
,如果這個位置存在值,那麼這個值的結果是已存在值 + 參數值。 istore_3
將操作數棧中的數移動到局部變數表的3號位置。
以下是這三行代碼的示意圖:
9-12行的位元組碼的作用原理和4-8行的作用原理基本相同:
9: iinc 2, 1
12: iload_2
13: istore 4
istore 4
的作用是將操作數棧中的值存儲到局部變數表4號位置。
以下是這三行代碼的示意圖:
接下來15-30行是和系統輸出有關的。其中第30行iload_3
在局部變數表中(這個值為2)值移動到操作數棧頂供系統輸出,事實上iload_3
的值正好對應源代碼中變數result1
的值。也就是說,result1
輸出結果就是iload_3
的數值2。
同理,iload 4
就是第二個要輸出的值,在局部變數表中第4個位置存儲的值正好是3,而輸出的變數名是result2
,因此result2
的輸出結果是3。
三、i++
和++i
性能分析
i++
和++i
主要用在普通for
迴圈上,那麼我們就將二者用在for
迴圈上,迴圈相同的次數,從位元組碼的角度進行分析。
以下是使用i++
和++i
的兩個for
迴圈文件:
/**
* i++在for迴圈的使用
*
* @author ZhaoCong
* @date 2023-10-21 16:14:33
*/
public class LoopTest1 {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
}
}
}
/**
* ++i在for迴圈的使用
*
* @author ZhaoCong
* @date 2023-10-21 16:15:17
*/
public class LoopTest2 {
public static void main(String[] args) {
for (int i = 0; i < 100; ++i) {
}
}
}
執行編譯命令以後,我們來查看兩個文件的位元組碼:
仔細觀察這兩個位元組碼文件內容,我們發現在兩個文件main
方法的位元組碼內容完全相同。由此可見,兩種方式執行for
迴圈的效率是相同的。