1. 程式人生 > >Java中類,物件,方法的記憶體分配

Java中類,物件,方法的記憶體分配

以下針對引用資料型別:
在記憶體中,類是靜態的概念,它存在於記憶體中的CodeSegment中。
當我們使用new關鍵字生成物件時,JVM根據類的程式碼,去堆記憶體中開闢一塊控制元件,存放該物件,該物件擁有一些屬性,擁有一些方法。但是同一個類的物件和物件之間並不是沒有聯絡的,看下面的例子:

class Student{
    static String schoolName;
    String name;
    int age;

    void speak(String name){
        System.out.println(name);
    }

    void
read(){ } }
class Test{
    public statis void main(String[] args){
        Student a = new Student();
        Student b = new Student();
    }
}

在上面的例子中,生成了兩個Student物件,兩個物件擁有各自的name和age屬性,而schoolName屬性由於是static的,被所有的物件所共有,而且每個物件都有權更改這個屬性。
而對於方法speak()和read()來講,它們被所有的物件所共有,但並不是說哪個物件擁有這些方法。方法僅僅是一種邏輯片段而已,它不是實物,嚴格來講不能被擁有。
方法存在的意義就是對物件進行修改。(我的理解)
上述speak()方法,雖然兩個物件都擁有相同的方法,但是由於其操作的物件不同,所以執行起來的效果也不同。再說一次,方法是看不見摸不著的,它僅僅是一種邏輯操作!只有當作用於具體的物件時,方法才具體化了!


方法在不執行的時候不佔用記憶體空間,只有在執行的時候才會佔用記憶體空間。
就好比說一個人會翻跟斗,他翻跟斗的時候是需要空間的,但是他不翻跟斗的時候是不需要額外的空間的。但是不管他翻不翻跟斗,他始終是具有翻跟斗的技能的。

Java中的記憶體佈局(其他面向物件的語言也是如此)

Java中的記憶體空間分為四種:
1. code segment
儲存class檔案的內容,即我們寫的程式碼。
2. data segment
儲存靜態變數
3. heap segment
堆空間,儲存new出來的物件
4. stack segment
棧空間,儲存引用型別的引用(注意,這裡儲存的不一定是物件所處的實體地址,但是一定能夠根據這個內容在堆中找到對應的物件),區域性變數和方法執行時的方法的程式碼

以上面的speak(String name)方法的呼叫為例來分析下記憶體:
呼叫該方法時,首先在棧控制元件開闢了一塊區域存放name引用。然後將傳入的那個物件的“地址”賦值給這個引用。於是出現了什麼情況?兩個引用指向同一個物件。而我們操作物件時是通過物件的引用來執行操作,所以當一個物件有一個以上的引用同時指向它時,就會出現一些比較混亂的事情了。

通過馬士兵在課上講的一個小例子來看看:

public class Test {

    public static void main(String[] args) {
        Test test = new Test();
        int data = 10;
        BirthDate b1 = new BirthDate(5,4,1993);
        BirthDate b2 = new BirthDate(25,5,1992);

        test.change(data);
        test.change1(b1);
        test.change2(b2);
        System.out.println(data);
        b1.display();
        b2.display();
    }

    void change(int i){
        i = 100;
    }

    void change1(BirthDate b){
        b = new BirthDate(1,1,1);
    }

    void change2(BirthDate b){
        b.setDay(100);
    }
}

class BirthDate{
    int day;
    int month;
    int year;

    BirthDate(int day,int month,int year){
        this.day = day;
        this.month = month;
        this.year = year;
    }

    public int getDay() {
        return day;
    }
    public void setDay(int day) {
        this.day = day;
    }
    public int getMonth() {
        return month;
    }
    public void setMonth(int month) {
        this.month = month;
    }
    public int getYear() {
        return year;
    }
    public void setYear(int year) {
        this.year = year;
    }

    public void display(){
        System.out.println("day = "+day+"\nmonth = "+month+"\nyear = "+year);
    }
}

輸出為:

10
day = 5
month = 4
year = 1993
day = 100
month = 5
year = 1992

從輸出結果來看看發生了什麼:
1. 呼叫change(int i)
此時在棧空間中新建了一個i,並把data的值複製給i。這個時候在棧空間中有兩個int型別的數,二者的值雖然都為10,但是二者毫無關係,棧空間中有兩個10.
在方法體中對i進行賦值,該操作是對i進行的,並不影響data,所以當方法結束時data還是原來的data,連地址都沒變一下。同時在方法結束時i自動從棧空間中消失。
2. 呼叫change1(BirthDate b)
此時在棧空間中新建一個引用b,在呼叫該方法的時候,將傳進來的引用的值複製給b,即b和b1擁有相同的內容,指向同一個物件。在方法體中對b又進行賦值操作,首先在堆空間中new出一個新的物件,然後將b改為指向這個新的物件。該操作也未影響b1。方法結束後,引用b消失,剛才new出來的新物件成了垃圾,等待GC的回收。
3. 呼叫change2(BirthDate b)
同上,先在對中新建一個引用,名為b,然後將其指向b2所指向的物件。注意,此時呼叫了該物件的方法,修改了部分屬性,所以此操作改變了這個物件,而b2也指向這個物件,所以最後b2的輸出發生了變化。

從上面的三個方法可以看出,當方法中的引數列表不為空時:
- 如果引數是引用資料型別,該方法執行的過程首先是建立若干個引用,然後將這些引用的值和傳進來的引用的值一一對應複製。複製完之後,傳進來的引數(引用)的工作也就完成了。
- 如果引數是基本資料型別,那麼首先在棧中建立對應數量個變數,將這些變數的值和傳進來的引數一一對應複製。複製完之後也沒有外面什麼事了。

綜上:傳參時要注意,如果對傳進去的引數(實際上是引用)進行了重新的賦值操作,那麼該方法應該有一個返回值,否則該方法是沒有意義的,如同上面的change1()。

  • 方法一般有兩個作用:1. 對某變數進行改變。 2. 根據傳進來的引數返回另一變數。
  • 如果一個方法不想有返回值,只是想對某變數進行改變,不要將該物件作為引數傳進去,而直接在方法中獲得其訪問許可權然後直接更改。方法的引數列表為空。
  • 如果一個方法要有返回值,最好先在方法內部new一個臨時變數,先將傳進來的引數複製一下,邏輯執行完後,把臨時變數return出去。

20170620更新:
其實以上問題涉及到的東西是值傳遞與引用傳遞。在C++中二者都有,但是在Java中只有值傳遞。具體到實踐中分兩種情況:
- 傳遞的是基本資料型別:
其實傳遞的是值的拷貝。在方法中對值進行操作,並不影響傳進去的那個值。如上面的change()方法,傳值進去時只是按照data的樣子重新建立了一個i,本質上data和i除了值相同以外,是兩個獨立的個體。

  • 傳遞的是陣列物件或者其他物件:
    實際上傳遞的是物件的引用,但是並不是把引用傳過去,而是把引用複製過去。就像上面的change1()方法一樣,其本質是將傳參b1這個引用的值複製給引用b。b1和b除了值相同外,是兩個獨立的個體。但是由於二者值相同,所以指向了堆記憶體中的同一個物件,二者都可以用來操作物件。

總結一下:
傳值,傳的都是棧中所儲存的東西的拷貝。如果傳進去的東西是基本資料型別,那麼就直接複製一份,對其操作不影響原來的資料。
如果傳進去的是一個引用,那麼其實也是複製一份,所以指向同一物件。當操作這個引用時,改變了這個引用所指向的物件,看起來會讓人覺得當時傳進去的是物件本身,不然怎麼在方法中對其修改會改變原本的物件呢?其實這是個假象。時刻記住,傳進函式的都是棧記憶體中的東西,堆記憶體的東西是不會被傳進去的。而函式內部能不能改變原來物件的值,就要看你是不是保持了原來傳進去的引用所指向的物件沒變。

PS:才疏學淺,如有錯誤請指出,謝謝!