自動內存管理是 CLR
在執行托管代碼過程中提供的一項功能。CLR
的GC (Garbage Collection, 垃圾回收器, 下同)
可以自動幫助程序分配或釋放內存。對于開發者來說,這意味著開發者并不需要寫額外的代碼來處理內存管理的事情了,同時這也避免了比較常見的錯誤,比如:
- 忘記釋放內存
- 釋放已經釋放過的內存
- 訪問一個已經被釋放的內存
本文將會討論 GC
在分配和釋放內存時是如何工作的。
分配內存
當建立一個新的線程時,CLR
將會為該線程準備一個連續的內存區域——托管堆。托管堆將會維護一個指針,該指針的地址將會是下一個對象生成的地址。在托管堆剛生成時,該地址會指向整個堆的基地址。所有的引用類型將會在堆中分配內存。程序生成第一個引用類型對象的時候,該類型的內存將會被分配在托管堆的基地址中。當程序創建下一個對象的時候,GC
會立即在第一個對象的后面分配一段內存,以此類推。
托管堆的內存分配比非托管的內存分配快,托管堆在分配內存時,僅相當于給一個指針加了一個數而已,此過程就像在棧中分配內存一樣快。不僅如此,由于托管堆中內存分配是連續的,并且對象存儲的位置也是連續的,這樣做會加快程序訪問對象的速度。
釋放內存
GC
的優化引擎將會針對程序選擇一個最佳的時間來進行垃圾回收操作。回收內存的時候,程序不用的對象將會被回收。GC
將會檢查程序的“根”。每一個托管的應用程序都有一個一組“根”。每一個根都會存一個托管堆中對象的引用,或者被設置為null
。程序的根包括static
成員、線程棧中的局部變量與方法的參數、CPU 寄存器。GC
將會拿到一個根的列表,其中包括被 JIT
激活的根和 正在被 CLR
維護的根。GC
將會根據這個列表生成一個圖,這個圖里包含所有可以從根訪問到的對象。
如上文所述,不在圖中的對象將不會被程序的 “根” 引用,GC
將考慮回收這些對象。在回收的過程中,GC
會先檢查托管堆,找到所有無法訪問到的對象;之后,GC
會使用內存拷貝功能來整理所有正常的對象,讓它們仍在在一個連續的內存空間內;最后一步很重要,GC
會更正程序中的“根”,并且將托管堆中的指針指向最后一個對象的后面。注意,只有當不可訪問對象到達一定數量的時候才會進行內存整理的操作。如果托管堆中所有的對象均能正常訪問,整理內存的操作是沒有必要的。
為了提升性能,運行時會把一個大對象拆分到兩個堆中存放,并且,GC
將會自動釋放大對象的內存。然而,為了防止移動內存中的大對象,這種情況下的內存是未經整理的。
托管堆的生命周期(Generations)
和性能
為了優化 GC
的性能,托管堆中的空間被分為三個部分:G0
(generation 0,下同
),G1
和G2
。CLR
采用的 GC
方案已經涵蓋了軟件行業中的各種情況。該方案基于以下前提:
- 在回收托管堆時,只回收其中一部分內存比回收全部內存快
- 較新的對象通常比較老的對象的壽命短
- 較新的對象之間有在彼此之間產生聯系的傾向,并幾乎在同一時間被程序引用。
GC
會把較新的對象放在 G0
中。在程序運行的初期結束后,G0
中未被回收的對象將會被“升級”,并被移動至 G1
和 G2
中(對象升級的過程將會在下文中討論)
。由于回收部分內存較快,托管堆將會優先回收具體的某一個時期(Generation)
,而不是回收一整個托管堆。
在 CLR
實際工作中,GC
將會在 G0
沒有足夠空間時回收內存。當程序在 G0
滿了的時候嘗試創建對象時,GC
發現 G0
中沒有空間了,所以它將會嘗試回收 G0
中的空間。
由于最先創建的對象壽命通常會比較短,故G0
中的對象會有更大的幾率被回收,所以我們并沒有直接回收整個托管堆的空間,這么做是一種比較高效的方式,在 G0
中進行局部的內存回收通常會釋放足夠的空間給新創建的對象。
在回收 G0
之后,GC
將會整理托管堆中的內存本文“釋放內存”一章提到過
。由于已經留存下來的對象更傾向于有較長的生命周期,所以 GC
要把 G0
中的對象提升至更高的時期(genearation)
中。所以,GC
也沒有必要在整理 G0
之后再去檢查 G1
和 G2
中的對象了。
在 GC
整理完 G0
并且將其中的對象提升至 G1
之后,它將會使用 G0
中剩余的空間來創建新的對象,直到 G0
中又沒有剩余空間時,GC
需要決定是否需要回收較老的時期(generation)
中的對象。比如,如果 GC
在 G0
中無法回收足夠的空間去創建新的對象時,GC
將會先后在 G1
和 G2
中觸發 垃圾回收的過程。如果這么做仍然無法滿足新創建對象的需求時,GC
將會回收 G2
,G1
,G0
中的內存,并且將 G0
中的剩余對象全部升級到 G1
中,G1
中剩余的對象全部升級到 G2
中。由于垃圾回收只支持三個時期(generations)
,所有 G2
中的對象將會繼續呆在 G2
中,直到其中的對象變得不可到達時,才回收這些對象。
給非托管代碼釋放內存
應用程序生成的對象都可以依賴 GC
來完成自動垃圾回收。然而,非托管資源需要自己手動管理對象的生命周期。典型的非托管資源的應用就是文件句柄file handle
,窗口句柄window handle
,或者網絡操作。即使 GC
可以追蹤包含非托管對象的托管對象,但是它并不知道到底如何清理非托管的對象。
當你試圖在托管對象中中引用非托管的對象時,應該自己實現當前對象的Dispose
方法,并且在該方法中清理非托管的對象。你可以在該對象的使用者結束使用該對象的時候釋放分配給該對象的內存。當使用包含非托管資源的托管對象時,一定要在必要的時候調用Dispose
方法以確保該托管對象已經被釋放。關于如何實現Dispose
方法,你可以參照 垃圾回收 一文
翻譯錯誤之處,請指出,感激不盡~