做者:Allen B. Downeyhtml
原文:Chapter 6 Memory managementgit
譯者:飛龍程序員
協議:CC BY-NC-SA 4.0github
C提供了4種用於動態內存分配的函數:數組
malloc
,它接受表示字節單位的大小的整數,返回指向新分配的、(至少)爲指定大小的內存塊的指針。若是不能知足要求,它會返回特殊的值爲NULL
的指針。瀏覽器
calloc
,它和malloc
同樣,除了它會清空新分配的空間。也就是說,它會設置塊中全部字節爲0。緩存
free
,它接受指向以前分配的內存塊的指針,並會釋放它。也就是說,使這塊空間可用於將來的分配。安全
realloc
,它接受指向以前分配的內存塊的指針,和一個新的大小。它使用新的大小來分配內存塊,將舊內存塊中的數據複製到新內存塊中,釋放舊內存塊,並返回指向新內存塊的指針。數據結構
這套API是出了名的易錯和苛刻。內存管理是設計大型系統中,最具備挑戰性的一部分,它正是許多現代語言提供高階內存管理特性,例如垃圾回收的緣由。函數
C的內存管理API有點像Jasper Beardly,動畫片《辛普森一家》中的一個配角,他是一個嚴厲的代課老師,喜歡體罰別人,並使用戒尺懲罰任何違規行爲。
下面是一些應受到懲罰的程序行爲:
若是你訪問任何沒有分配的內存塊,就應受到懲罰。
若是你釋放了某個內存塊以後再訪問它,就應受到懲罰。
若是你嘗試釋放一個沒有分配的內存塊,就應受到懲罰。
若是你釋放屢次相同的內存塊,就應受到懲罰。
若是你使用沒有分配或者已經釋放的內存塊調用realloc
,就應受到懲罰。
這些規則聽起來好像不難遵循,可是在一個大型程序中,一塊內存可能由程序一部分分配,在另外一個部分中使用,以後在其餘部分中釋放。因此一部分中的變化也須要其它部分跟着變化。
同時,同一個內存塊在程序的不一樣部分中,也可能有許多別名或者引用。這些內存塊在全部引用再也不使用時,才應該被釋放。正確處理這件事情一般須要細心的分析程序的全部部分,這很是困難,而且與良好的軟件工程的基本原則相違背。
理論上,每一個分配內存的函數都應包含內存如何釋放的信息,做爲接口文檔的一部分。成熟的庫一般作得很好,可是實際上,軟件工程的實踐一般不是這樣理想化的。
內存錯誤很是難以發現,由於這些症狀是不可預測的,這使得事情更加糟糕,例如:
若是從未分配的內存塊中讀取值,系統可能會檢測到錯誤,觸發叫作「段錯誤」的運行時錯誤,而且停止程序。這個結果很是合理,由於它表示程序所讀取的位置會致使錯誤。可是,遺憾的是,這種結果很是少見。更一般的是,程序讀取了未分配的內存塊,而沒有檢測到錯誤,程序所讀取的未分配內存正好儲存在一塊特定區域中。若是這個值沒有解釋爲正確的類型,結果可能會難以解釋。例如,若是你讀取字符串中的字節,將它們解釋爲浮點數,你可能會獲得一個無效的數值,很是大或很是小的數值。若是你向函數傳遞它沒法處理的值,結果會很是怪異。
若是你向未分配的內存塊中寫入值,會更加糟糕。由於在值被寫入以後,須要很長時間值才能被讀取而且發生錯誤。此時尋找問題來源就會很是困難。事情還可能更加糟糕!C風格內存管理的一個最廣泛的問題是,用於實現malloc
和free
的數據結構(咱們將會看到)一般和分配的內存塊儲存在一塊兒。因此若是你無心中越過動態分配塊的末尾寫入值,你就可能破壞了這些數據結構。系統一般直到最後纔會檢測到這種問題,當你調用malloc
或free
時,這些函數會因爲一些謎之緣由調用失敗。
你應該從中總結出一條規律,就是安全的內存管理須要設計和規範。若是你編寫了一個分配內存的庫或模塊,你應該同時提供釋放它的接口,而且內存管理從開始就應該做爲API設計的一部分。
若是你使用了分配內存的庫,你應該按照規範使用API。例如,若是庫提供了分配和釋放儲存空間的函數,你應該一塊兒使用或都不使用它們。例如,不要在不是malloc
分配的內存塊上調用free
。你應該避免在程序的不一樣部分中持有相同內存塊的多個引用。
一般在安全的內存管理和性能之間有個權衡。例如,內存錯誤的的最廣泛來源是數組的越界寫入。這一問題的最顯然的解決方法就是邊界檢查。也就是說,每次對數組的訪問都應該檢查下標是否越界。提供數組結構的高階庫一般會進行邊界檢查。可是C風格數據和大多數底層庫不會這樣作。
有一種可能會也可能不會受到懲罰的內存錯誤。若是你分配了一塊內存,而且沒有釋放它,就會產生「內存泄漏」。
對於一些程序,內存泄露是OK的。若是你的程序分配內存,對其執行計算,以後退出,這可能就不須要釋放內存。當程序退出時,全部分配的內存都會由操做系統釋放。在退出前當即釋放內存彷佛很負責任,可是一般很浪費時間。
可是若是一個程序運行了很長時間,而且泄露內存的話,它的內存總量會無限增加。此時會發生一些事情:
某個時候,系統會耗完全部物理內存。在沒有虛擬內存的系統上,下一次的malloc
調用會失敗,返回NULL
。
在帶有虛擬內存的系統上,操做系統能夠將其它進程的頁面從內存移動到磁盤上,以後分配更多空間給泄露的進程。我會在7.8節解釋這一機制。
單個進程可能有內存總量的限制,超過它的話,malloc
會返回NULL
。
最後,進程可能會用完它的虛擬地址空間(或者可用的部分)。以後,沒有更多的地址可分配,malloc
會返回NULL
。
若是malloc
返回了NULL
,可是你仍舊把它當成分配的內存塊進行訪問,你會獲得段錯誤。所以,在使用以前檢查malloc
的結果是個很好的習慣。一種選擇是在每一個malloc
調用以後添加一個條件判斷,就像這樣:
void *p = malloc(size); if (p == NULL) { perror("malloc failed"); exit(-1); }
perror
在stdio.h
中聲明,它會打印出關於最後發生的錯誤的錯誤信息和額外的信息。
exit
在stdlib.h
中聲明,會使進程終止。它的參數是一個表示進程如何終止的狀態碼。按照慣例,狀態碼0表示一般終止,-1表示錯誤狀況。有時其它狀態碼用於表示不一樣的錯誤狀況。
錯誤檢查的代碼十分討厭,而且使程序難以閱讀。可是你能夠經過將庫函數的調用和錯誤檢查包裝在你本身的函數中,來解決這個問題。例如,下面是檢查返回值的malloc
包裝:
void *check_malloc(int size) { void *p = malloc (size); if (p == NULL) { perror("malloc failed"); exit(-1); } return p; }
因爲內存管理很是困難,多數大型程序,例如Web瀏覽器都會泄露內存。你可使用Unix的ps
和top
工具來查看系統上的哪一個程序佔用了最多的內存。
當進程啓動時,系統爲text
段、靜態分配的數據、棧和堆分配空間,堆中含有動態分配的數據。
並非全部程序都動態分配數據,因此堆的大小可能很小,或者爲0。最開始堆只含有一個空閒塊。
malloc
調用時,它會檢查這個空閒塊是否足夠大。若是不是,它會向系統請求更多內存。作這件事的函數叫作sbrk
,它設置「程序中斷點」(program break),你能夠將其看作一個指向堆底部的指針。
譯者注:
sbrk
是Linux上的系統API,Windows上使用HeapAlloc
和HeapFree
來管理堆區。
sbrk
調用時,它分配的新的物理內存頁,更新進程的頁表,並設置程序中斷點。
理論上,程序應該直接調用sbrk
(而不是經過malloc
),而且本身管理堆區。可是malloc
易於使用,而且對於大多數內存使用模式,它運行速度快而且高效利用內存。
爲了實現內存管理API,多數Linux系統都使用ptmalloc
,它基於dlmalloc
,由Doug Lea編寫。一篇描述這個實現要素的論文可在http://gee.cs.oswego.edu/dl/html/malloc.html訪問。
對於程序員來講,須要注意的最重要的要素是:
malloc
在運行時一般不依賴塊的大小,可是可能取決於空閒塊的數量。free
一般很快,和空閒塊的數量無關。由於calloc
會清空塊中的每一個字節,執行時間取決於塊的大小(以及空閒塊的數量)。realloc
有時很快,若是新的大小比以前更小,或者空間可用於擴展示有的內存塊。不然,它須要從舊內存塊中複製數據到新內存塊,這種狀況下,執行時間取決於舊內存塊的大小。
邊界標籤:當malloc
分配一個快時,它在頭部和尾部添加空間來儲存塊的信息,包括它的大小和狀態(分配仍是釋放)。這些數據位叫作「邊界標籤」。使用這些標籤,malloc
就能夠從任何塊移動到內存中上一個或下一個塊。此外,空閒塊會連接到一個雙向鏈表中,因此每一個空閒塊也包含指向「空閒鏈表」中下一個塊和上一個塊的指針。邊界標籤和空閒鏈表指針構成了malloc
的內部數據結構。這些數據結構穿插在程序的數據中,因此程序錯誤很容易破壞它們。
空間開銷:邊界標籤和空閒鏈表指針也佔據空間。最小的內存塊大小在大多數系統上是16字節。因此對於很是小的內存塊,malloc
在空間上並不高效。若是你的程序須要大量的小型數據結構,將它們分配在數組中可能更高效一些。
碎片:若是你以多種大小分配和釋放塊,堆區就會變得碎片化。也就是說,空閒空間會打碎成許多小型片斷。碎片很是浪費空間,它也會經過使緩存效率低下來下降程序的速度。
裝箱和緩存:空閒鏈表在箱子中以大小排序,因此當malloc
搜索特定大小的內存塊時,它知道應該在哪一個箱子中尋找。因此若是你釋放了一塊內存,以後當即以相同大小分配一塊內存,malloc
一般會很快。