從JVM底層原理分析數值交換那些事
基礎數據類型交換
這個話題,需要從最最基礎的一道題目說起,看題目:以下程式碼a和b的值會交換么:
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並沒有交換:
integerA=1 , integerB=2
但是原因呢?先看這張圖,先來說說Java虛擬機的結構:
運行時區域主要分為:
- 執行緒私有:
- 程式計數器:
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
方法,也就是類方法,所以不會有當前對象的指針。
如果是普通方法,那麼局部變數表裡面會多出一個局部變數this
。
如何證明這個東西真的存在呢?我們大概看看位元組碼,因為局部變數在編譯的時候就確定了,運行期不會變化的。下面是IDEA
插件jclasslib
查看的:
上面的圖,我們在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並沒有變。
那同樣來從位元組碼看,會發現確實有3個局部變數在局部變數表內,並且他們的數值都是int類型。
而swap(a,b)
執行結束之後,該方法的堆棧會被彈出虛擬機棧,此時虛擬機棧又剩下main()
方法的棧幀,由於基礎數據類型的數值相當於存在局部變數中,swap(a,b)
棧幀中的局部變數不會影響main()
方法的棧幀中的局部變數,所以,就算你在swap(a,b)
中交換了,也不會變。
基礎包裝數據類型交換
將上面的數據類型換成包裝類型,也就是Integer
對象,結果會如何呢?
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;
}
結果還是一樣,交換無效:
a=1 , b=2
這個怎麼解釋呢?
對象類型已經不是基礎數據類型了,局部變數表裡面的變數存的不是數值,而是對象的引用了。先用jclasslib
查看一下位元組碼裡面的局部變數表,發現其實和上面差不多,只是描述符變了,從int
變成Integer
。
但是和基礎數據類型不同的是,局部變數裡面存在的其實是堆裡面真實的對象的引用地址,通過這個地址可以找到對象,比如,執行main()
函數的時候,虛擬機棧如下:
假設 a 裡面記錄的是 1001 ,去堆裡面找地址為 1001 的對象,對象裡面存了數值1。b 裡面記錄的是 1002 ,去堆裡面找地址為 1002 的對象,對象裡面存了數值2。
而執行swapInteger(a,b)
的時候,但是還沒有交換的時候,相當於把 局部變數複製了一份:
而兩者交換之後,其實是SwapInteger(a,b)
棧幀中的a裡面存的地址引用變了,指向了b,但是b裡面的,指向了a。
而swapInteger()
執行結束之後,其實swapInteger(a,b)
的棧幀會退出虛擬機棧,只留下main()
的棧幀。
這時候,a其實還是指向1,b還是指向2,因此,交換是沒有起效果的。
String,StringBuffer,自定義對象交換
一開始,我以為String
不會變是因為final
修飾的,但是實際上,不變是對的,但是不是這個原因。原因和上面的差不多。
String
是不可變的,只是說堆/常量池內的數據本身不可變。但是引用還是一樣的,和上面分析的Integer
一樣。
其實StringBuffer
和自定義對象都一樣,局部變數表記憶體在的都是引用,所以交換是不會變化的,因為swap()
函數內的棧幀不會影響調用它的函數的棧幀。
不行我們來測試一下,用事實說話:
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 + '\'' +
'}';
}
}
執行結果,證明交換確實沒有起效果。
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等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花里胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查找資料。遺漏或者錯誤之處,還望指正。
平日時間寶貴,只能使用晚上以及周末時間學習寫作,關注我,我們一起成長吧~