1. 程式人生 > >從JVM底層原理分析數值交換那些事

從JVM底層原理分析數值交換那些事

## 基礎資料型別交換 這個話題,需要從最最基礎的一道題目說起,看題目:以下程式碼a和b的值會交換麼: ```java public static void main(String[] args) { int a = 1, b = 2; swapInt(a, b); System.out.println("a=" + a + " , b=" + b); } private static void swapInt(int a, int b) { int temp = a; a = b; b = temp; } ``` 結果估計大家都知道,a和b並沒有交換: ```shell integerA=1 , integerB=2 ``` 但是原因呢?先看這張圖,先來說說Java虛擬機器的結構: ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210124193442.png) 執行時區域主要分為: - 執行緒私有: - 程式計數器:`Program Count Register`,執行緒私有,沒有垃圾回收 - 虛擬機器棧:`VM Stack`,執行緒私有,沒有垃圾回收 - 本地方法棧:`Native Method Stack`,執行緒私有,沒有垃圾回收 - 執行緒共享: - 方法區:`Method Area`,以`HotSpot`為例,`JDK1.8`後元空間取代方法區,有垃圾回收。 - 堆:`Heap`,垃圾回收最重要的地方。 和這個程式碼相關的主要是虛擬機器棧,也叫方法棧,是每一個執行緒私有的。 生命週期和執行緒一樣,主要是記錄該執行緒Java方法執行的記憶體模型。虛擬機器棧裡面放著好多**棧幀**。**注意虛擬機器棧,對應是Java方法,不包括本地方法。** **一個Java方法執行會建立一個棧幀**,一個棧幀主要儲存: - 區域性變量表 - 運算元棧 - 動態連結 - 方法出口 每一個方法呼叫的時候,就相當於將一個**棧幀**放到虛擬機器棧中(入棧),方法執行完成的時候,就是對應著將該棧幀從虛擬機器棧中彈出(出棧)。 每一個執行緒有一個自己的虛擬機器棧,這樣就不會混起來,如果不是執行緒獨立的話,會造成呼叫混亂。 大家平時說的java記憶體分為堆和棧,其實就是為了簡便的不太嚴謹的說法,他們說的棧一般是指虛擬機器棧,或者虛擬機器棧裡面的區域性變量表。 區域性變量表一般存放著以下資料: - 基本資料型別(`boolean`,`byte`,`char`,`short`,`int`,`float`,`long`,`double`) - 物件引用(reference型別,不一定是物件本身,可能是一個物件起始地址的引用指標,或者一個代表物件的控制代碼,或者與物件相關的位置) - returAddress(指向了一條位元組碼指令的地址) 區域性變量表記憶體大小編譯期間確定,執行期間不會變化。空間衡量我們叫Slot(區域性變數空間)。64位的long和double會佔用2個Slot,其他的資料型別佔用1個Slot。 上面的方法呼叫的時候,實際上棧幀是這樣的,呼叫main()函式的時候,會往虛擬機器棧裡面放一個棧幀,棧幀裡面我們主要關注區域性變量表,傳入的引數也會當成區域性變數,所以第一個區域性變數就是引數`args`,由於這個是`static`方法,也就是類方法,所以不會有當前物件的指標。 ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203153821.png) 如果是普通方法,那麼區域性變量表裡面會多出一個區域性變數`this`。 如何證明這個東西真的存在呢?我們大概看看位元組碼,因為區域性變數在編譯的時候就確定了,執行期不會變化的。下面是`IDEA`外掛`jclasslib`檢視的: ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203154054.png) 上面的圖,我們在`main()`方法的區域性變量表中,確實看到了三個變數:`args`,`a`,`b`。 **那在main()方法裡面呼叫了swapInt(a, b)呢?** 那堆疊裡面就會放入`swapInt(a,b)`的棧幀,**相當於把a和b區域性變數複製了一份**,變成下面這樣,由於裡面一共有三個區域性變數: - a:引數 - b:引數 - temp:函式內臨時變數 a和b交換之後,其實`swapInt(a,b)`的棧幀變了,a變為2,b變為1,但是`main()`棧幀的a和b並沒有變。 ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203160207.png) 那同樣來從位元組碼看,會發現確實有3個區域性變數在區域性變量表內,並且他們的數值都是int型別。 ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203154758.png) 而`swap(a,b)`執行結束之後,該方法的堆疊會被彈出虛擬機器棧,此時虛擬機器棧又剩下`main()`方法的棧幀,由於基礎資料型別的數值相當於存在區域性變數中,`swap(a,b)`棧幀中的區域性變數不會影響`main()`方法的棧幀中的區域性變數,所以,就算你在`swap(a,b)`中交換了,也不會變。 ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203163654.png) ## 基礎包裝資料型別交換 將上面的資料型別換成包裝型別,也就是`Integer`物件,結果會如何呢? ```java public static void main(String[] args) { Integer a = 1, b = 2; swapInteger(a, b); System.out.println("a=" + a + " , b=" + b); } private static void swapInteger(Integer a, Integer b) { Integer temp = a; a = b; b = temp; } ``` 結果還是一樣,交換無效: ```shell a=1 , b=2 ``` 這個怎麼解釋呢? 物件型別已經不是基礎資料型別了,區域性變量表裡面的變數存的不是數值,而是物件的引用了。先用`jclasslib`檢視一下位元組碼裡面的區域性變量表,發現其實和上面差不多,只是描述符變了,從`int`變成`Integer`。 ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203161807.png) ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203161835.png) 但是和基礎資料型別不同的是,區域性變數裡面存在的其實是堆裡面真實的物件的引用地址,通過這個地址可以找到物件,比如,執行`main()`函式的時候,虛擬機器棧如下: 假設 a 裡面記錄的是 1001 ,去堆裡面找地址為 1001 的物件,物件裡面存了數值1。b 裡面記錄的是 1002 ,去堆裡面找地址為 1002 的物件,物件裡面存了數值2。 ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203163949.png) 而執行`swapInteger(a,b)`的時候,但是還沒有交換的時候,相當於把 區域性變數複製了一份: ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203164756.png) 而兩者交換之後,其實是`SwapInteger(a,b)`棧幀中的a裡面存的地址引用變了,指向了b,但是b裡面的,指向了a。 ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203165041.png) 而`swapInteger()`執行結束之後,其實`swapInteger(a,b)`的棧幀會退出虛擬機器棧,只留下`main()`的棧幀。 ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203163949.png) 這時候,a其實還是指向1,b還是指向2,因此,交換是沒有起效果的。 ## String,StringBuffer,自定義物件交換 一開始,我以為`String`不會變是因為`final`修飾的,但是實際上,不變是對的,但是不是這個原因。原因和上面的差不多。 `String`是不可變的,只是說堆/常量池內的資料本身不可變。但是引用還是一樣的,和上面分析的`Integer`一樣。 ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203165910.png) 其實`StringBuffer`和自定義物件都一樣,區域性變量表記憶體在的都是引用,所以交換是不會變化的,因為`swap()`函式內的棧幀不會影響呼叫它的函式的棧幀。 不行我們來測試一下,用事實說話: ```java public static void main(String[] args) { String a = new String("1"), b = new String("2"); swapString(a, b); System.out.println("a=" + a + " , b=" + b); StringBuffer stringBuffer1 = new StringBuffer("1"), stringBuffer2 = new StringBuffer("2"); swapStringBuffer(stringBuffer1, stringBuffer2); System.out.println("stringBuffer1=" + stringBuffer1 + " , stringBuffer2=" + stringBuffer2); Person person1 = new Person("person1"); Person person2 = new Person("person2"); swapObject(person1,person2); System.out.println("person1=" + person1 + " , person2=" + person2); } private static void swapString(String s1,String s2){ String temp = s1; s1 = s2; s2 = temp; } private static void swapStringBuffer(StringBuffer s1,StringBuffer s2){ StringBuffer temp = s1; s1 = s2; s2 = temp; } private static void swapObject(Person p1,Person p2){ Person temp = p1; p1 = p2; p2 = temp; } class Person{ String name; public Person(String name){ this.name = name; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + '}'; } } ``` 執行結果,證明交換確實沒有起效果。 ```java a=1 , b=2 stringBuffer1=1 , stringBuffer2=2 person1=Person{name='person1'} , person2=Person{name='person2'} ``` ## 總結 基礎資料型別交換,棧幀裡面存的是區域性變數的數值,交換的時候,兩個棧幀不會干擾,`swap(a,b)`執行完成退出棧幀後,`main()`的區域性變量表還是以前的,所以不會變。 物件型別交換,棧幀裡面存的是物件的地址引用,交換的時候,只是`swap(a,b)`的區域性變量表的區域性變數裡面存的引用地址變化了,同樣`swap(a,b)`執行完成退出棧幀後,`main()`的區域性變量表還是以前的,所以不會變。 所以不管怎麼交換都是不會變的。 **【作者簡介】**: 秦懷,公眾號【**秦懷雜貨店**】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。個人寫作方向:Java原始碼解析,JDBC,Mybatis,Spring,redis,分散式,劍指Offer,LeetCode等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花裡胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查詢資料。遺漏或者錯誤之處,還望指正。 [2020年我寫了什麼?](http://aphysia.cn/archives/2020) [開源程式設計筆記](https://damaer.github.io/Coding/#/) 平日時間寶貴,只能使用晚上以及週末時間學習寫作,關注我,我們一起成