Note
這篇文章中的例子講的都是 C 語言,但如果你是寫 Java 的,也誠心的建議你可以耐心地閱讀,就算程式碼看不懂也沒關係。最主要的是要知道這篇文章中講的關於 Heap 的配置、有一種變數存的是記憶體位置……等等的概念,這些事情對你了解 Java 的物件是如何存在記憶體當中相當有幫助。
Heap 是什麼?
上一篇文章中,我們談到了記憶體分為兩種,以及 Stack 與區域變數之間的關係,這一篇文章,我們就來看看到底 Heap 是什麼東西。
現在你應該已經知道了,在 C / Java 裡,宣告在 C 的函式或 Java 的方法裡的區域變數,全都是放在 Stack 上,而這些變數在函式執行結束之後,就會被消滅,你不能也不應該去存取他。
但有的時候,我們確實需要讓某些資料在函式結束之後也存在,甚至可以被其他的函式所存取,那應該要怎麼辦呢?
這個時候,就是 Heap 的回合了!
你可以把 Heap 想像成是一個由程式語言 runtime 管理的空間銀行,當你需要一些空間來存放資料的時候,你就和程式語言 runtime 講說:「喂,我要 30MB 的記憶體空間。」
接著程式語言 runtime 就會把 30MB 的記憶體空間騰出來,並且把這塊記憶體空間的開頭位址給你,讓你知道你所要到的記憶體到底是在哪。而等你不需要用的時候,再把這塊 30MB 的記憶體還給程式語言 runtime ,讓其他人可以使用這塊記憶體。
在這部份,事實上程式語言 runtime 有一套非常複雜的管理機制,來盡量確保可以在記憶體當中挖出 30MB 來給你,但現在不需要擔心那麼多,只要知道程式語言 runtime 會幫你處理這些事,而他真的挖不出來的時候也會告訴你記憶體爆炸了就好。
C 語言如何在 Heap 挖資料
就像我們在上一篇所講的,C 語言所有的區域變數都是存在 Stack 上,那我們要怎麼樣操作從 Heap 挖出來的記憶體呢?
別緊張,還記得剛剛說過,當我們像程式語言 runtime 要記憶體的時候,程式語言 runtime 會給我們那塊記憶體的開頭位址嗎? 我們只要把那塊記憶體的開頭位置存到區域變數當中,當要用到 Heap 上的資料時,再從那個變數裡存的記憶體位置去拿就好。
這種存放記憶體位置的變數,在 C 語言裡就稱為「指標」,英文叫 Pointer。所以以後寫 C 語言遇到 Pointer 請不要再害怕了,他和一般的變數沒有什麼不同,就只是存的是記憶體位址而已。
至於要怎麼樣向程式語言 runtime 要 Heap 中的記憶體呢?
在 C 語言的標準函式庫裡有各種不同的函式可以用,但你最常見到的應該會是 void * malloc(size_t size) 這一個,你傳你所需要的大小進去,C 語言就會幫你向程式語言 runtime 要 Heap 中的記憶體,然後把指到該塊記憶體開頭的 pointer 給你。
Note
在 C 語言裡,void * 代表「指到不一定是什麼資料型態的資料的指標」
廢話不多說,我們直接來看 C 語言的程式要怎麼寫:
#include <stdlib.h>
#include <stdio.h>
void requireHeap()
{
int * heapInt;;
heapInt = (int *) malloc(sizeof(int));
printf("heapInt: %d\n", heapInt);
printf("*heapInt: %d\n", *heapInt);
*heapInt = 10;
printf("*heapInt: %d\n", *heapInt);
printf("&(*heapInt): %d\n", &(*heapInt));
}
int main()
{
requireHeap();
}
上面的程式碼進行的事情很簡單,我們在 requireHeap 裡用 malloc 向程式語言 runtime 要了一塊大小為 sizeof(int) 的記憶體,也就是一塊大小可以存放一個整數的記憶體空間,這個時候 malloc 會回傳一個 pointer 給我們,因為我們已經知道我們要存的是整數,所以我們用 (int *) 將回傳來的 pointer 強制轉型成為指到整數資料的 pointer。
換話句話,在 C 語言裡面,你需要指到什麼樣的資料型態的 pointer,就在該型態後面加個 * 號就好,例如 int * 是指到 int 類型的資料,double * 是指到 double 類型的資料,依此類推……
這個時候要記得 heapInt 存的是記憶體位置,所以如果你執行這個程式的話,會看到如下的輸出:
brianhsu@USBGentoo ~ $ ./a.out
heapInt: 25042960
*heapInt: 0
*heapInt: 10
&(*heapInt): 25042960
brianhsu@USBGentoo ~ $ ./a.out
heapInt: 8540176
*heapInt: 0
*heapInt: 10
&(*heapInt): 8540176
你會看到每次執行時,heapInt 都會出現奇怪的數字,而且這個數字每次都不太一樣。
這是因為 heapInt 存的是記憶體位置,而你每次向程式語言 runtime 要記憶體的時候,要到的地方都會不太一樣!
接著問題就來了,既然 heapInt 裡存的只是記憶體的位置,那我們要怎麼樣才能存東西到那塊記憶體裡面去呢?在 C 語言之中,我們可以在一個型態為 pointer 的變數前加上 * 號來達成這件事,通常我們叫這個動作為「取值」或是 dereference。
所以我們可以在 printf 用 *heapInt 來印出這塊 Heap 記憶體中的內容,這個時候是 0。
緊接著再用 *heapInt = 10 來將 10 這個值塞進這塊 Heap 當中,這個時候再印一次 *heapInt 的話,就會是 10 了。
透過這樣子用 pointer 進行 dereference 的動作,我們就可以對向程式語言 runtime 要來的 Heap 空間做讀取和寫入的動作。
最後,由於當你寫 (*heapInt) 的時候,指的就是程式語言 runtime 配置的那塊記憶體,所以如果你再用 C 裡「取記憶體地址」的 & 運算元的話,就會發現 &(*heapInt) 取到的地址和 malloc 回傳的地址是相同的。
接著我們試著把這種個程式的 Call stack 畫出來看看……記得 Stack 和 Heap 是不同的東西,所以要分開來畫喔。
記憶體洩漏
到了這裡,不知道你有沒有發現上面的四張圖有點怪怪的呢?
沒錯,在第四張圖裡面,我們的 requireHeap() 函式返回之後,用來指到程式語言 runtime 給我們的記憶體用的 heapInt 變數已經消失了,但是記憶體位址 8540176 的地方還存著一個整數 10,但卻沒有人可以用到那塊記憶體!
想想看,如果我們在 main() 裡面呼叫了 requireHeap 一百次的話,或發生什麼事情呢?
嗯……你答對了,Heap 上被挖出了一百個整數十的洞,可是接下來卻沒有任何人可以用到這些你挖出來的記憶體空間。
這個狀況,我們就叫他記憶體洩漏,或 Memory Leak,因為我們的記憶體就像壞掉的水龍頭一樣,在不知不覺當中一點一滴的流失掉了。
下面是如果我們在 main() 中呼叫 requireHeap() 三次之後,在 main() 函式返回讓程式結束掉之前的記憶體狀態,可以看見被挖掉了三個整數的空間。
C 語言如何釋放記憶體
上面這種「記憶體一點一點被吃掉」的情況當然是我們所不樂見的,所以我們必須在使用完 Heap 之後將記憶體還給程式語言 runtime ,讓他可以被其他人使用才行。
在 C 語言裡面要達成這件事,靠的就是 free(void *) 這個函式,我們只要傳給他當初 malloc 回傳給我們,指到的 Heap 開頭的指標就好。
所以我們修改我們的 requireHeap() 函式如下:
void requireHeap()
{
int * heapInt;;
heapInt = (int *) malloc(sizeof(int));
printf("heapInt: %d\n", heapInt);
printf("*heapInt: %d\n", *heapInt);
*heapInt = 10;
printf("*heapInt: %d\n", *heapInt);
printf("&(*heapInt): %d\n", &(*heapInt));
free(heapInt);
}
這樣的話,現在我們的 Call Stack 會長這個樣子:
關於 C 記憶體操作的一些小陷阱
如果你在上面的程式當中,試著在把 heapInt 給 free 掉後,又用 *heapInt 來取得值,可能會發現他可以正常執行,但是讀出來的值可能怪怪的,例如在我的機器上讀出來會是 0。
這是因為 C 語對於存取已經 free 掉的 Heap 空間的定義是 undefined,也就是說只有神才知道發生什麼事。
但因為這塊記憶體空間已經被宣告為 free 了,也就是說他隨時都會配置給其他人,其他人隨時都可能寫資料到這塊記憶體上把你的資料蓋掉,所以你絕對不應該存取已經被 Free 掉的空間 。
另外,如果你試著呼叫兩次 free(heapInt) 則程式會馬上當掉,這是因為雖然這樣的行為也是 undefined,但大部份的 C 語言的 runtime 會做檢查,在發生這件事時把你的程式結束掉。例如若你是在 Linux 下,glibc 就會丟出 double free or corruption (fasttop) 這個錯誤。
小結
這一次我們講的是 Heap 的基本觀念,以及存放在 Heap 上的資料的特性,總結如下:
- Heap 像是由程式語言 runtime 管理的銀行,你可以向他要記憶體和還記憶體
- 要操作 Heap 上的資料,靠的是指到該 Heap 的記憶體位置的指標
- 在 C 語言要到的 Heap 空間一定要 free 掉,不然會有記憶體洩漏
- 已經被 free 掉的 Heap 隨時可能分配給其他人,絕對不要再去存取他。
有了這些基本的概念,接下來我們終於可以開始討論 Java 了!下一章我們會討論 Java 怎麼樣使用 Heap 來存放物件,還有他怎麼對付 Memory Leak 的問題。
參考資料
你可以在下面找到 Java 對於詳細記憶體操作的規範,還有 C 的作者之一寫的 The C Programming Language,兩個都是當你想要詳細了解 C / Java 對了你的記憶體幹了什麼好事時的很好的參考資料:
回響