從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並無交換:git

integerA=1 , integerB=2

可是緣由呢?先看這張圖,先來講說Java虛擬機的結構:
github

運行時區域主要分爲:redis

  • 線程私有:
    • 程序計數器:Program Count Register,線程私有,沒有垃圾回收
    • 虛擬機棧:VM Stack,線程私有,沒有垃圾回收
    • 本地方法棧:Native Method Stack,線程私有,沒有垃圾回收
  • 線程共享:
    • 方法區:Method Area,以HotSpot爲例,JDK1.8後元空間取代方法區,有垃圾回收。
    • 堆:Heap,垃圾回收最重要的地方。

和這個代碼相關的主要是虛擬機棧,也叫方法棧,是每個線程私有的。
生命週期和線程同樣,主要是記錄該線程Java方法執行的內存模型。虛擬機棧裏面放着好多棧幀注意虛擬機棧,對應是Java方法,不包括本地方法。shell

一個Java方法執行會建立一個棧幀,一個棧幀主要存儲:編程

  • 局部變量表
  • 操做數棧
  • 動態連接
  • 方法出口
    每個方法調用的時候,就至關於將一個棧幀放到虛擬機棧中(入棧),方法執行完成的時候,就是對應着將該棧幀從虛擬機棧中彈出(出棧)。

每個線程有一個本身的虛擬機棧,這樣就不會混起來,若是不是線程獨立的話,會形成調用混亂。分佈式

你們平時說的java內存分爲堆和棧,其實就是爲了簡便的不太嚴謹的說法,他們說的棧通常是指虛擬機棧,或者虛擬機棧裏面的局部變量表。ide

局部變量表通常存放着如下數據:函數

  • 基本數據類型(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,ab

那在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等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花裏胡哨,大多寫系列文章,不能保證我寫的都徹底正確,可是我保證所寫的均通過實踐或者查找資料。遺漏或者錯誤之處,還望指正。

2020年我寫了什麼?

開源編程筆記

平日時間寶貴,只能使用晚上以及週末時間學習寫做,關注我,咱們一塊兒成長吧~

相關文章
相關標籤/搜索