.NET
記憶體管理是自動進行的,包括以下幾個過程
- 記憶體分配
- 記憶體釋放
- 代(
Generations
) - 非託管資源的記憶體釋放
記憶體分配
當初始化一個程式時,執行時會為該程式分配一個連續的地址空間區域——即為託管堆。
託管堆就像一個管家一樣,始終持有一把鑰匙的鑰匙(一個指標)——下一個空房間(可用空間首地址,即為下一個物件分配的空間的開始位置)。當然,最開始管家手中的鑰匙,是大門的鑰匙(即託管堆的基地址)。
所有的引用型別,都是在託管堆上分配的;而值型別的空間則在棧上分配。
需要注意的是:
- 引用型別在託管堆上分配空間(房間)之後,對這個空間的引用(鑰匙)則是放在棧上(管家手中)的
- 對引用(鑰匙)的複製,屬於簡單的複製(淺拷貝)。因為這也僅僅是多了一把鑰匙而已,這兩把鑰匙都只能開同一個房間(兩個引用都指向同一個地址空間)
在託管堆中進行記憶體的分配,還可以帶來效能的優勢:
- 執行時為新物件分配記憶體時,通過不斷的更改指標的值(更改管家手中的鑰匙),而不是從系統記憶體中新分配記憶體(即建造一個新房間)。這樣,記憶體分配的速度幾乎可以同在棧上分配一樣快了
- 記憶體分配的連續性,可以做到快速的訪問這些物件
不過,這裡有個前提
託管堆中的記憶體足夠應用程式使用。如果不夠,執行時將會頻繁的進行
GC
,這會對效能造成很大的影響。比如在Unity3D
的開發中,頻繁的GC
可能會造成遊戲畫面不連續。
記憶體的釋放
GC
會根據物件的分配,來決定該物件回收的最佳時機。
它通過檢查應用程式的根來確定不再使用的物件,每個應用程式都有一組根(包含執行緒堆疊和CPU
暫存器上的靜態欄位、區域性變數和引數),每個根要麼引用託管堆中的物件,要麼被設定為null
。
GC
通過訪問JIT
和執行時維護的活動根的列表來檢查應用程式的根(可訪問性檢查),同時建立一個可訪問的物件圖。不在該圖中的物件,即表示不可達(無法從根訪問),將被GC
視為垃圾,並釋放為這些物件分配的記憶體。
在回收過程中,如果發現大量不可訪問的物件,則會使用記憶體複製功能來壓縮記憶體中可訪問的物件:移動物件,以保證可訪問的物件在一塊連續的記憶體空間內。
同時,對託管堆指標進行更正(重新為管家拿一把鑰匙)。這樣的好處是,可以讓剩下的記憶體空間連續。
值得注意的是,為了效能,在進行記憶體複製的時候,將不會處理託管堆中的大型物件(如影像)。
其一,這些大物件的移動可能會花很長時間;
其二,GC
的過程中,會掛起正在執行的執行緒,如果在這過程中去移動這些大物件,則可能會造成程式假死。
代(Generations
)
為了優化GC
效能,託管堆被分為了三代:第0
代、第1
代和第2
代。
其垃圾回收演算法的原理如下:
- 壓縮一部分記憶體要比壓縮整個託管堆快
- 較新的物件的生存期較短,較老的物件的生存期較長
- 較新的物件與其他物件有更大的關聯性,且基本上會在某一時間段內被應用程式訪問
鑑於以上原理,有以下的過程:
- 新的物件儲存在第
0
代 - 老的物件如果未被回收,則升級為第
1
代和第2
代 GC
在第0
代已滿的時候,將回收第0
代中的物件(新物件),而這往往可以回收足夠多的記憶體- 在第
0
代回收的過程中,未被回收的物件,將會升級為第1
代 - 若第
0
代的回收中,未能回收到足夠的記憶體,這時,GC
將對第1
代的物件進行回收 - 以此類推,第
1
代未被回收的,將會升級為第2
代,等等。
非託管資源的記憶體釋放
非託管資源與託管資源不同,它們的記憶體需要我們顯式的釋放。
其釋放方式,除了上一篇文章溫故之.NET託管資源與非託管資源中介紹的方式外,還有另外一種方式。
這種方式我們將在下一篇文章【溫故之.NET垃圾回收】中一一道來。
小結
每次GC
時,做了什麼
- 檢索堆上的每個物件
- 搜尋所有當前物件引用以確定堆上的物件是否仍在作用域內
- 不在作用域內的物件被標記為刪除
- 刪除被標記的物件並將記憶體返回給堆
故堆上的物件越多,程式碼中的引用數越多,GC
就越費時。
另外需要注意的是,每次GC
時,其都會掛起當前正在執行的執行緒,這肯定會對效能有影響,因此需要避免過多的GC
。
何時觸發GC
- 堆分配時堆上的可用記憶體不足時觸發
GC
:順序為第0
代、第1
代、第2
代,具體可見上面【代(Generations
)】 GC
會不時的自動執行(頻率因平臺而異),此即為“合適的時機”,但第一條中的情況下,一定會執行- 手動
GC
:呼叫GC.Collect()
或GC.Collect(int generation)
- 作業系統記憶體不足時,會觸發
GC
至此,本節內容講解完畢。歡迎關注公眾號【嘿嘿的學習日記】,所有的文章,都會在公眾號首發,Thank you~