這是雨痕的的文章,原文在這裡。
關於 CLR 記憶體管理員模式,各種書籍和網路文章各執一詞,十分混亂。大多數時候也只是泛泛提到 "託管堆" 和 "堆棧" 而已,其實際過程非常複雜。鑒於沒有一個統一的說法,我也不能保證本文的準確性。
當 CLR 建立執行線程時,會依據 mainfest 中的 ".stackreserve 0x00100000" 指令為當前線程分配 1MB 的呼叫堆疊(Call Stack)記憶體。呼叫堆疊不同於託管堆(Managed Heap),它不負責儲存託管引用對象,只是用來維護方法調用次序,執行 IL 指令。而當我們使用 call、callvirt 等指令調用一個方法時,CLR 會在呼叫堆疊中分配一個稱之為堆疊框架(Stack Frame) 的記憶體塊用於執行目標方法代碼。就是一個堆疊框架的記憶體。
,每個堆疊框架中包含三塊記憶體:參數表 (Argument Table) 用於儲存方法參數,內部變數表 (Local Variable Table) 儲存方法局部變數(.locals init),而 計算堆棧 (Evaluation Stack) 則是 IL 代碼執列區域。執行 IL 指令前,相關指令資料(Data)或其地址(Address)被壓入計算堆棧,然後執行指令並彈出計算結果,執行下一個指令時繼續重複這樣的動作,直到方法調用結束。這三部分記憶體共同構成了一個方法的記憶體執行環境,需要注意的是其大小單位並不是 byte,而是 slot。每個 slot 可以儲存一個託管單元(對象),我們可以在方法體中使用 .maxstack 指令控制當前方法計算堆棧的大小。
.method private hidebysig static void Test1(int32& x) cil managed
{
.maxstack 8
}
回到方法調用流程,當我們使用 call 等指令調用一個方法時,CLR 為目標方法在呼叫堆疊上新分配一個堆疊框架,並將方法參數從當前方法的計算堆棧彈出壓入目標方法的參數表中,接著執行流程跳轉到目標方法。當執行 .locals init 指令時,會在其內部變數表中建立相應的內部變數,然後使用目標方法的計算堆棧完成 IL 指令執行。當目標方法 IL 指令結束時,其傳回值(如果有)會被彈出(pop)並壓入(push)最初調用者的計算堆棧中,ret 指令將控制權返回給調用者,目標方法堆疊框架被拋棄,所佔用記憶體被釋放。
相關概念
棧(Stack)和堆(Heap)不同,它是一種線性結構,而堆是一種鏈式結構。棧按照先進後出 (LIFO - Last In First Out) 的原理工作,通過一個指標來維護記憶體佔用。當我們釋放一段記憶體時,只要將指標回複到某個起始位置即可,因此它的執行效率非常高。而託管堆(Managed Heap)是動態分配的記憶體地區,由 GC 進行管理,每個進程中的所有線程共用一個託管堆。
.NET CLR 和 Java VM 都是一種被稱之為堆棧式虛擬機器(Stack-Base VM)的技術,它的執行機制和我們所熟悉的 80x86 執行方式有所不同,它並沒有寄存器的概念,他們的指令集(Instruction Set) 都是採用堆棧進行運算,首先將資料(Data)壓入棧,然後進行操作。此處的堆棧就是我們上面所說的計算堆棧(Evaluation Stack)。