堆記憶體是什麼?
堆記憶體是在應用程式內動態分配記憶體時常用的可用記憶體池。之所以使用 “堆記憶體” 這個術語,是因為動態記憶體分配的大多數實現均使用稱為堆 的一種二進位制樹型資料結構。動態記憶體分配是在應用程式執行時顯式分配和取消分配記憶體(或儲存)的一種方法。在本文中,堆管理器 這個術語用於描述處理應用程式動態記憶體分配的軟體。
堆管理器執行兩種主要操作。第一種操作是分配。這種操作將保留給定大小的一個儲存塊,並返回所保留儲存塊的指標。儲存塊的惟一所有權將賦予應用程式,應用程式可以使用該儲存塊來實現它所需要的任何目的。第二種操作是取消分配。此操作會將之前已分配好的儲存塊返回給堆管理器。取消分配一個儲存塊時,塊的所有權也將返還給堆管理器。此後,堆管理器可以使用該儲存塊繼續分配。
堆管理器經常會提供的第三種操作是重新分配。這種操作會重新調整給定儲存塊的大小,並返回儲存塊的指標(可能經過更新)。重新分配操作並不屬於嚴格需要的操作(因為可以通過基本的分配和取消分配操作予以實現),但堆管理器通常會提供這種操作。
在 IBM i® 上,共有兩種不同型別的堆儲存:單層儲存和 teraspace 儲存。應用程式使用的儲存型別由建立程式時指定的程式屬性決定。預設情況下使用的是單層儲存。如果需要使用 teraspace 儲存,必須使用特殊編譯選項和程式建立選項。除此之外,也可以使用應用程式程式設計介面 (API) 在單層儲存應用程式內分配和取消分配 teraspace 儲存。如需進一步瞭解這兩種型別的儲存,請參閱 ILE 概念 手冊中的 “Teraspace 和單層儲存” 部分。
兩種型別的堆儲存之間存在某些重大差異。從堆中分配單層儲存時,最多隻能分配 16 MB 的記憶體。從堆中分配 teraspace 儲存時,可以分配數 TB 大小的儲存。必須使用十六位元組的指標來定址單層儲存。對於支援八位元組指標的語言(例如 C 和 C++),可以使用八位元組指標定址 teraspace 儲存。單層儲存堆的總大小限制為每作業 4 GB。teraspace 堆不存在這類限制,teraspace 儲存堆的總大小僅受限於可用的系統儲存數量。
如何分配或取消分配堆儲存
儘管每種語言均有不同的分配和取消分配方法,但所有整合語言環境 (ILE) 語言均可使用堆記憶體。C 語言中使用的是 malloc()
和free()
函式,C++ 中使用的是 new
和 delete
操作符,而 RPG 中使用的是 %ALLOC
內建函式和 DEALLOC
操作。儘管 COBOL 和 CL 沒有管理堆記憶體的內建函式,但 ILE 模型允許從任意 ILE 語言呼叫函式,因此這些語言可以呼叫 malloc()
和 free()
函式來管理堆記憶體。實際上,所有 ILE 語言均可呼叫 malloc()
和 free()
函式來管理堆記憶體。下面的幾個示例展示了所有這些語言中對堆記憶體的分配、使用和取消分配。
示例 1 是使用 C 語言編寫的。
示例 1 – C 中的堆記憶體
/* To compile: CRTBNDC PGM(EXAMPLE1) */
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <stdlib.h> #include <string.h> int main (int argc, char *argv[]) { /* allocate 16 bytes of storage from the heap */ char *ptr = (char*)malloc(16); /* set the allocated storage */ memcpy(ptr, "abcdefghijklmnop", 16); /* deallocate storage */ free(ptr); return 0; } |
示例 2 是使用 C++ 語言編寫的。
示例 2 – C++ 中的堆記憶體
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/* To compile: CRTBNDCPP PGM(EXAMPLE2) */ #include <string.h> int main (int argc, char *argv[]) { /* allocate 16 bytes of storage from the heap */ char *ptr = new char[16]; /* set the allocated storage */ memcpy(ptr, "abcdefghijklmnop", 16); /* deallocate storage */ delete [] ptr; return 0; } |
示例 3 是使用 RPG 語言編寫的。
示例 3 – RPG 中的堆記憶體
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
* To compile: CRTBNDRPG PGM(EXAMPLE3) H dftactgrp(*no) D ptr@ S * D based_data S 16A BASED(ptr@) D sz S 5 0 INZ(16) /free // allocate 16 bytes of storage from the heap ptr@ = %ALLOC(sz); // set the allocated storage based_data = 'abcdefghijklmnop'; // deallocate storage DEALLOC ptr@; *inlr = '1'; |
示例 4 是使用 COBOL 語言編寫的。
示例 4 – COBOL 中的堆記憶體
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
PROCESS NOMONOPRC. IDENTIFICATION DIVISION. * To compile: CRTBNDCBL PGM(EXAMPLE4) BNDDIR(QC2LE) PROGRAM-ID. EXAMPLE4. ENVIRONMENT DIVISION. DATA DIVISION. WORKING-STORAGE SECTION. 01 ptr POINTER. 01 sz PIC S9(9) BINARY. LINKAGE SECTION. 77 based-data PIC X(16). PROCEDURE DIVISION. MAIN-LINE SECTION. 001-MAIN-FLOW. MOVE 16 TO sz. * allocate 16 bytes of storage from the heap CALL LINKAGE PRC "malloc" USING BY VALUE sz RETURNING ptr. * set the allocated storage SET ADDRESS OF based-data TO ptr. MOVE "abcdefghijklmnop" TO based-data. * deallocate storage CALL LINKAGE PRC "free" USING BY VALUE ptr. STOP RUN. |
示例 5 是使用 CL 語言編寫的。
示例 5 – CL 中的堆記憶體
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/* To compile: CRTBNDCL PGM(EXAMPLE5) */ PGM DCL VAR(&PTR) TYPE(*PTR) DCL VAR(&SZ) TYPE(*INT) VALUE(16) DCL VAR(&BASED_DATA) TYPE(*CHAR) LEN(16) STG(*BASED) + BASPTR(&PTR) /* allocate 16 bytes of storage from the heap */ CALLPRC PRC('malloc') PARM( (&SZ *BYVAL) ) RTNVAL(&PTR) /* set the allocated storage */ CHGVAR VAR(&BASED_DATA) VALUE('abcdefghijklmnop') /* deallocate storage */ CALLPRC PRC('free') PARM( (&PTR *BYVAL) ) ENDPGM |
這些示例僅分配了極少的堆儲存(16 位元組)。通常情況下,所分配的儲存塊要比這大得多。舉例來說,考慮這樣一個事實,CL 變數的最大大小為 32767 位元組。利用堆儲存和 *PTR 變數,可以在 CL 中管理更大的儲存塊。
堆記憶體的常見問題
堆分配和取消分配必須由應用程式顯式執行,因此可能會出現這些操作使用不當的問題。堆記憶體使用不當的常見場景包括:寫入的資料量超過了所分配的儲存量(記憶體寫入越界),讀取的資料量超過了已分配記憶體的大小(記憶體讀取越界),寫入儲存或讀取儲存時使用的是此前已經取消分配的儲存(重用已取消分配的記憶體),取消分配儲存超過一次(重複取消分配),在記憶體不再使用時未能取消分配記憶體(記憶體洩漏)。
下面的示例程式展示了這些堆記憶體問題。
前兩個場景涉及到了堆分配的大小。分配時將為堆管理器提供所需的儲存大小,它將返回足夠大的儲存塊,以滿足所請求的大小。如果應用程式未能正確計算堆大小,那麼有時會意外地讀取或寫入不屬於當前分配儲存的一部分堆儲存。這可能會給應用程式帶來問題。
示例 6 展示了 C++ 中的記憶體寫入越界問題。堆僅分配了 16 位元組的大小,但總計寫入了 17 位元組的資料。strcpy()
函式不僅會複製字串的 16 個位元組,而且還會追加一個零位元組字尾(NULL 字元)。
示例 6 – C++ 中的記憶體寫入越界
1 2 3 4 5 6 7 8 9 10 11 12 |
/* To compile: CRTBNDCPP PGM(EXAMPLE6) */ #include <string.h> int main (int argc, char *argv[]) { char *ptr = new char[16]; /* strcpy() copies the trailing NULL character */ strcpy(ptr, "abcdefghijklmnop"); /* memory overwrite */ delete [] ptr; return 0; } |
示例 7 展示了 C 語言中的記憶體寫入越界。堆僅分配了 13 位元組的大小,但總計寫入了 14 位元組的資料。strcpy()
函式不僅會複製字串的 13 個位元組,而且還會追加一個零位元組字尾(NULL 字元)。
示例 7 – C 中的記憶體寫入越界
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/* To compile: CRTBNDC PGM(EXAMPLE7) */ #include <stdlib.h> #include <string.h> int main (int argc, char *argv[]) { char *ptr = (char*)malloc(13); /* strcpy() copies the trailing NULL character */ strcpy(ptr, "abcdefghijklm"); /* memory overwrite */ free(ptr); return 0; } |
示例 8 展示了 RPG 中的記憶體寫入越界問題。堆僅分配了 13 位元組的大小,但總計寫入了 14 位元組的資料。基本變數的大小是 14 個位元組。
示例 8 – RPG 中的記憶體寫入越界
1 2 3 4 5 6 7 8 9 10 11 12 13 |
* To compile: CRTBNDRPG PGM(EXAMPLE8) H dftactgrp(*no) D ptr@ S * D message S 14A BASED(ptr@) /free ptr@ = %ALLOC(13); // the entire 14-byte message variable is // updated, including trailing blanks message = 'hello'; // memory overwrite DEALLOC ptr@; *inlr = '1'; |
下一個場景涉及過早地取消分配應用程式仍在使用的堆儲存。邏輯錯誤可能允許應用程式引用已經取消分配並返回給堆管理器進行重用的堆儲存。此類分配中引用的資料可能是所需資料,也可能不是所需資料,可能會導致應用程式中出現間歇性錯誤。
示例 9 是使用 C++ 編寫的,展示了寫入不再屬於已分配儲存的堆儲存的情況。
示例 9 – 取消分配的記憶體重用
1 2 3 4 5 6 7 8 9 10 11 |
/* To compile: CRTBNDCPP PGM(EXAMPLE9) */ #include <string.h> int main (int argc, char *argv[]) { char *ptr = new char[16]; delete [] ptr; strcpy(ptr, "abc"); /* reuse of deallocated memory */ return 0; } |
下一個場景涉及到多次取消分配相同的儲存。
示例 10 是使用 C 語言編寫的,展示了取消分配記憶體的重複呼叫。
示例 10 – 取消分配記憶體的重複呼叫
1 2 3 4 5 6 7 8 9 10 11 |
/* To compile: CRTBNDC PGM(EXAMPLE10) */ \#include <stdlib.h> int main (int argc, char *argv[]) { char *ptr = (char*)malloc(16); free(ptr); free(ptr); /* duplicate deallocation */ return 0; } |
最後一個場景是未能對某些已經分配的堆記憶體執行取消分配。這就叫做記憶體洩露,因為記憶體洩漏 到了堆以外的地方,無法再供應用程式使用。儘管記憶體不再被引用,但堆管理器並不瞭解該情況,也無法重用記憶體。記憶體洩漏會造成嚴重的問題。大量記憶體洩漏會導致效能問題,還有可能導致應用程式耗盡記憶體。在長期執行的應用程式中,這種問題尤為嚴重。在某些時候,應用程式結束之後(在啟用組終止時),作業系統會回收該應用程式的所有堆儲存,因此記憶體洩漏不再成為問題。
示例 11 是使用 RPG 編寫的,展示了記憶體洩漏的情況。儘管應用程式嘗試取消分配儲存,但傳遞給取消分配函式的指標值不是分配函式返回的原始指標,因此未能取消對記憶體的分配。
示例 11 – RPG 中的記憶體洩漏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
* To compile: CRTBNDRPG PGM(EXAMPLE11) H dftactgrp(*no) D ptr@ S * D name S 10A BASED(ptr@) D i S 5 0 /free // allocate enough storage for 100 names ptr@ = %ALLOC(%SIZE(name) * 100); // move along the storage one name at a time for i = 1 to 100; name = 'something'; ptr@ += %SIZE(name); endfor; // deallocate storage DEALLOC ptr@; // memory leak *inlr = '1'; |
不正確的堆使用會導致間歇性的應用程式故障、不當的應用程式行為,甚至損壞的資料。致使堆問題難以除錯的一個特徵是:堆錯誤往往不會造成直接後果。舉例來說,在緩衝區寫入越界的情況下,寫入的資料超出了已分配記憶體緩衝區的結尾處。當引用寫入越界、不再包含所需資料的記憶體時,問題徵兆要到很晚的時候才會顯現在應用程式中。
IBM i 堆記憶體管理器
IBM i 6.1 及更新版本中提供了三種堆記憶體管理器:預設記憶體管理器、Quick Pool 記憶體管理器和除錯記憶體管理器。對給定應用程式有效的記憶體管理器由應用程式執行時的 QIBM_MALLOC_TYPE
環境變數的設定控制。6.1 版本中的 PTF 5761SS1-SI33945 提供了訪問其他堆記憶體管理器的環境變數,IBM i 釋出版的後續版本也包含此類環境變數。在新增環境變數訪問的同時,還新增了除錯記憶體管理器。在版本 5.4 和 6.1 中,還可以呼叫 API 進行支援設定,從而使用 Quick Pool 記憶體管理器。如需檢視堆記憶體管理器支援的完整文件,請參閱 ILE C/C++ 執行時庫函式 手冊。
預設記憶體管理器
預設記憶體管理器是一種通用的記憶體管理器,它會嘗試平衡效能和記憶體需求。它為絕大多數應用程式提供了充足的效能,同時會嘗試最大程度地減少開銷所需的額外記憶體量。預設記憶體管理器是大多數應用程式的最佳選擇,預設情況下,記憶體管理器是啟用的。如果IBM_MALLOC_TYPE
環境變數尚未設定,或者被設定為無法識別的值,則會使用預設的記憶體管理器。
Quick Pool 記憶體管理器
Quick Pool 記憶體管理器會將記憶體拆分為一系列池,以便提高發出大量較小的分配請求的應用程式的效能。在啟用 Quick Pool 記憶體管理器時,將為處於給定分配大小範圍內的分配請求分配池中固定大小的單元。這些請求的處理速度將快於大小超出此範圍的請求。超出此範圍的分配請求將按照與預設記憶體管理器相同的方式處理。
預設情況下不會啟用 Quick Pool 記憶體管理器,但可以通過設定以下環境變數來啟用它:
1 |
QIBM_MALLOC_TYPE=QUICKPOOL |
也可以在應用程式中使用 API 呼叫啟用 Quick Pool 記憶體管理器。如需瞭解有關的更多資訊,請參閱 ILE C/C++ 執行時庫函式 手冊。
除錯記憶體管理器
除錯記憶體管理器主要用於查詢應用程式沒有正確使用堆的情況。它並未針對效能而優化,可能會對應用程式的效能造成負面影響。然而,它對於確定不當的堆使用情況很有價值。
除錯記憶體管理器檢測到的記憶體問題會導致以下兩種行為之一:
- 如果在發生不當使用之時檢測到問題,那麼將會生成一條機器檢查控制程式碼 (MCH) 異常訊息(通常是 MCH0601、MCH3402 或者 MCH6801)。在這種情況下,錯誤訊息通常會停止應用程式。
- 如果在不當使用已經發生之後才檢測到問題,則會生成一條 C2M1212 訊息。在這種情況下,訊息通常不會停止應用程式。
除錯記憶體管理器會通過兩種方式檢測記憶體問題:
- 首先,使用限制訪問記憶體頁面。在每次分配之前和之後使用一個限制訪問許可權的記憶體頁面。讓每個記憶體塊都與 16 位元組的邊界對齊,並儘可能地將它們放置在頁面結尾處。由於僅允許在頁面邊界處保護記憶體,所以這樣的對齊對於記憶體寫入越界和記憶體讀取越界的檢測效果最好。在一個限制訪問許可權的記憶體頁面中執行任何讀取或寫入操作時,會立即生成一條 MCH 異常。
- 第二,它會在每次分配前後使用一個填充位元組。在分配時,緊鄰每次分配的記憶體之前的幾個位元組會初始化為預設的位元組模式。在分配之時,如果分配的大小需要限制為 16 位元組的倍數,那麼緊鄰所分配記憶體之後的填充位元組也會初始化為預設的位元組模式。在所分配的記憶體取消分配時,將驗證填充位元組,確保其仍然包含預期的預設位元組模式。如果任何填充位元組被修改,那麼除錯記憶體管理器會生成一條 C2M1212 訊息,原因程式碼為 X’80000000’。
預設情況下不會啟用除錯記憶體管理器,但可以通過設定以下環境變數來啟用它:
1 |
QIBM_MALLOC_TYPE=DEBUG |
除錯堆記憶體的常見問題
上文列出了使用堆記憶體時的幾種常見問題,還給出了一些示例程式,展示了各種堆問題。除錯記憶體管理器允許檢測多種堆記憶體常見問題,包括:記憶體寫入越界、記憶體讀取越界、重用已取消分配的記憶體和重複的取消分配。除錯記憶體管理器不會檢測記憶體洩漏問題。ILE 應用程式內的記憶體洩漏檢測將在未來的文章中加以介紹。
在使用除錯記憶體管理器執行程式時,將描述展示堆問題的每個示例程式的行為。
示例 6 展示了一個記憶體寫入越界問題。其中展示了嘗試超越大小為 16 位元組的倍數的資料項末尾的一項寫入操作。將該示例編譯為一個單層儲存程式,並使用除錯記憶體管理器執行此程式,在發生記憶體寫入越界時,這會生成一條 MCH0601 訊息。將該示例編譯為一個 teraspace 儲存程式,並使用除錯記憶體管理器執行此程式,在發生記憶體寫入越界時,這會生成一條 MCH6801 訊息。無論出現哪種情況,錯誤訊息的細節都會指向執行記憶體寫入越界的語句。示例展示了記憶體寫入越界,但記憶體讀取越界也會得到相同的結果。
示例 7 展示了一個記憶體寫入越界問題。其中演示了未超越 16 位元組邊界的記憶體寫入操作。將該示例被編譯為一個單層儲存程式或 teraspace 程式,並使用除錯記憶體管理器執行該程式,這會得到一條 C2M1212 訊息,原因程式碼為 X’80000000’。訊息的細節將指向呼叫 free()
的語句。除錯記憶體管理器無法檢測到未超越 16 位元組邊界的記憶體讀取越界。
示例 8 展示了一個記憶體寫入越界問題。由於在預設情況下 RPG 應用程式不會啟用除錯記憶體管理器,因此未能檢測到記憶體寫入越界。IBM i 7.1 的一項增強為 RPG 新增了 ALLOC(*TERASPACE) 控制規範關鍵字。該關鍵字將通知 RPG 執行時為記憶體分配和取消分配使用 C 執行時 teraspace 堆函式,允許對 RPG 應用程式使用除錯記憶體管理器。完成上述操作之後,該示例將具備與示例 7 相同的行為。如需進一步瞭解 ALLOC 關鍵字的其他細節,請參閱 ILE RPG 語言參考 手冊。
示例 9 展示了寫入不再屬於已分配儲存的堆儲存的情況。將該示例編譯為一個單層儲存程式,並使用除錯記憶體管理器執行此程式,在發生記憶體寫入越界時,這會生成一條 MCH3402 訊息。將該示例編譯為一個 teraspace 儲存程式,並使用除錯記憶體管理器執行此程式,在發生記憶體寫入時,這會生成一條 MCH6801 訊息。無論出現哪種情況,錯誤訊息的細節都會指向執行記憶體寫入的語句。示例展示了記憶體寫入,但記憶體讀取也會得到相同的結果。
示例 10 展示了取消分配記憶體的重複呼叫。將該示例編譯為一個單層儲存程式或 teraspace 程式,並使用除錯記憶體管理器執行該程式,這會得到一條 C2M1212 訊息。訊息的細節將指向呼叫 free()
的語句。
示例 11 展示了一個記憶體洩漏問題。如前文所述,除錯記憶體管理器不會檢測記憶體洩漏問題。
如果未使用除錯記憶體管理器,則無法檢測到這些記憶體問題,同時還有可能導致間歇性的應用程式問題、不當的應用程式行為或損壞的資料。
破解堆的謎題
對於編寫和維護 ILE 應用程式而言,瞭解堆記憶體是什麼以及如何正確使用堆記憶體的能力極為重要。在明確認識常見堆問題的同時,利用除錯記憶體管理器即可輕鬆檢測到應用程式內的堆問題,迅速解決 IBM i ILE 應用程式內的堆記憶體問題。
本文內容最初是在 iProDeveloper 2010 年 8 月刊中釋出的。