淺談 C 語言和 Java 的記憶體管理(三)

<< 上一篇

Java 也有 Stack / Heap / Pointer 這些東西嗎?

前兩篇文章,我們提到了在 C 語言裡 Stack 與區域變數之間的關係,還有如何用 Pointer 來操作向程式語言 runtime 要回來的 Heap 空間。

你可能會覺得疑惑,這些東西在 Java 當中也有嗎?特別是 Pointer 這東西,大家不是都說 Java 因為沒有 Pointer 所以比 C++ 簡單好學嗎?

事實上,這些東西在 Java 中通通都有,只是 Java 把他隱藏得很好,讓你覺得這些東西好像不存在一樣,但如果你沒有這些觀念,卻會讓你覺得 Java 好像捉摸不定。

Java 變數的分類

如果你是 Java Programmer,應該相當習慣了當你在宣告某些變數的時候,他的型態開頭是小寫:

  • byte
  • short
  • int
  • long
  • char
  • float
  • double
  • boolean

你也應該習慣了,除了上述八種資料型態外,其他的資料型態應該要用大寫開頭,如果你看到一個變數的宣告是 myType x 這樣,應該立馬會覺得寫出這行程式碼的傢伙對 Java 一定不熟。

這是因為在 Java 裡變數分成兩大類,一種是 Primitive Type,也就是上述列出來的八個小寫開頭的資料型態,而其他所有的資料型態,不論他是啥 Java 類別或介面,一律都算做 Reference 資料型態,而在 Java 的慣例裡,Reference 型態的資料型別習慣上都是大寫開頭。

那 Primitive Type 和 Reference 有什麼不同呢?

簡單的來說如下:

  1. Primitive Type 的變數裡存的是「值」,例如一個 int x = 10,x 裡存的就是整數 10;一個 double y = 0.5,y 那塊記憶體裡存的就是 0.5。
  2. Reference 存的是某樣東西的記憶體地址

看到關鍵字了嗎?還記得有什麼東西存的也是記憶體地址嗎?答對了!就是 Pointer!

所以你現在知道為什麼大家說 Java 裡沒有 Pointer 這句話有問題了吧?因為 Java 裡確實有 Pointer,只是他的 Pointer 叫做 Reference,而且你還天天在用他……

最簡單的類別的宣告與物件配置

我們已經知道,在 Java 裡,你宣告在方法當中的變數,都是存在 Stack 上了,所以 Stack 在 Java 當中確實是有被用到沒有錯,但 Heap 呢?我從來沒在 Java 裡向 JVM 要 Heap 裡的空間的樣子啊?

沒錯,因為 Java 並不像 C 一樣允許你直接操作記憶體,但就某方面來說,你寫 Java 的時候用到 Heap 的機會比 C 還大很多--因為所有你 new 出來的 Java 物件全部都長在 Heap 上。

多說無益,我們直接來看程式碼吧:

public class SimpleObject
{
    private int x = 10;

    public static void main (String [] args)
    {
        SimpleObject object1;
        SimpleObject object2;

        object1 = new SimpleObject();
        object2 = new SimpleObject();

        object1.x = 20;

        System.out.println(object1.x);
        System.out.println(object2.x);
    }
}

這個程式非常簡單,我們定義了一個 SimpleObject 的類別,並且在 main() 裡建立了兩個 SimpleObject 物件,然後把 object1 物件的整數變數 x 設成 20。

這個時候的記憶體裡究竟發生了哪些事呢?這個時候你要試著回答:

  1. object1 和 object2 的變數放在記憶體的哪裡?
  2. 我們 new 出來的物件又放在記憶體的哪裡?
  3. 這兩個物件中的 x 變數又是在哪裡呢?

答案如下:

  1. object1 和 object2 都放在 Stack 上給 main() 函式的空間
  2. 這兩個物件都在 Heap 裡
  3. 因為 x 是成員變數,所以是跟著物件,既然物件在 Heap 裡,他們也應該會在 Heap 裡。

另外你也應該注意到了,你在寫 Java 的時候只有對於 Reference 型態的變數才會加上 . 後面接著某些東西,這個 . 就類似 C 的 Pointer 前加上 * 號做 deference 來取得 Pointer 指到的位置一樣。

換句話說,你也可以把 object1.x 記成像「找出 object1 裡存的地址所指到的 Heap 空間當中的 x」這樣的意思。

有了上述的概念之後,我們就可以試著把這個程式在執行時的記憶體狀態一步一步畫出來……

Stack Status
Stack Status
Stack Status
Stack Status

現在你應該很清楚自己在 Java 裡 new 一個物件出來的時候,會發什生麼事情了吧?

那再多一點成員變數呢?

你可能會問,在上面的例子裡 SimpleObject 只有一個 x 變數,如果我再加一個 y 變數上去,會變怎麼樣呢?其實也沒怎樣--就是在 Heap 上的物件大了一點,多給你一點空間放 y 變數而已。

例如下面的程式:

public class SimpleObject
{
    private int x = 10;
    private double y = 20.0;

    public static void main (String [] args)
    {
        SimpleObject object1;
        SimpleObject object2;

        object1 = new SimpleObject();
        object2 = new SimpleObject();

        object1.x = 20;

        System.out.println(object1.x);
        System.out.println(object2.x);
    }
}

在執行完 object1.x = 20 這行之後,記憶體的狀況會如下圖所示:

Stack Status

那如果成員變數裡也有 Reference 型態呢?

上面的兩個程式碼裡,我們的 SimpleObject 類別的成員變數都只有 int 和 double 這些以小寫開頭的基本資料型態,那如果我們的類別比較複雜,不只有這些數值的資料,還有其他物件呢?

以下面的程式碼來看,我們的記憶體到底會變得怎麼樣呢?

class AnotherObject
{
    private int x = 30;
}

public class SimpleObject
{
    private int x = 10;
    private double y = 20.0;
    private AnotherObject z = new AnotherObject();

    public static void main (String [] args)
    {
        SimpleObject object1;
        SimpleObject object2;

        object1 = new SimpleObject();
        object2 = new SimpleObject();

        object1.x = 20;

        System.out.println(object1.x);
        System.out.println(object2.x);
    }
}

只要你記得以下兩點:

  1. 所有 Java 物件都在 Heap 上
  2. 所有非基本資料型態的變數,都是 Reference,存的都是地址

那你應該很容易得出以下的結論:

  1. 現在 Heap 上有四個物件:
    1. object1 指到的 SimpleObject
    2. object2 指到的 SimpleObject
    3. object1 指到的 SimpleObject 裡的 z 指到的 AnotherObject
    4. object2 指到的 SimpleObject 裡的 z 指到的 AnotherObject
  2. 既然 object1 和 object2 裡面的 z 都是 Reference,他們存的一定是記憶體地址!
  3. 所以 object1.z 存的是上述的第 3 個物件
  4. 而 object2.z 存的是上述的第四個物件

有了這些線索,我們就可以很簡單地把記憶體當中的情況畫出來了:

Stack Status

小結

這一次我們講的是關於 Java 中物件和 Heap 的關係,你可以看到 Java 是如何存放我們所 new 出來的物件,和他與區域變數之間的關係。

這一節的重點如下:

  1. Java 的資料型態分成 primitive type 和 reference type 兩種,其中 reference type 其實就是指標,存的是記憶體地址。
  2. Java 裡所有的物件都是存在 Heap 上面。

到了這邊,你應該可以了解其實當你 new 一個 Java 物件的時候,其實就和 C 語言的 malloc 一樣,是在向程式語言的 runtime(這裡的話就是 JVM)要 Heap 的空間。

可是你可能也注意到了,你在寫 Java 程式的時候,從來沒有寫過「把記憶體還給 JVM」這樣的程式碼,頂多是把某個 Reference 變數設為 null 而已,下次我們會詳細來看為什麼在寫 Java 時,不需要明確地把從 Heap 挖來的空間歸還給 JVM。

下一篇 >>

回響