前言
今天我們來共同學習一下CLR的垃圾回收機制,這對我們寫出健壯性的代碼很有幫助,也許有人會認為多此一舉,認為垃圾回收交給CLR就行,我不用關心這個,誠然,大多數情況下是這樣的,但是,我們今天討論的是程序的健壯性以及能夠快速定位那些神出鬼沒的問題。
一個例子
static void Main(string[] args)
{
Timer timer = new Timer(OnTimer,null,0,1000);
Console.ReadLine();
}
private static void OnTimer(object state)
{
Console.WriteLine(1);
}
看一下上面的代碼,大家認為在release模式下,會打印出來幾個1?
可能會有兩種答案:
- 無限多個,1s一個
- 不確定幾個
再看下列代碼:
static void Main(string[] args)
{
Timer timer = new Timer(OnTimer,null,0,1000);
Console.ReadLine();
}
private static void OnTimer(object state)
{
Console.WriteLine(1);
GC.Collect();
}
這次能打印出來幾個1呢?是不是還是兩種答案呢?
這里我先說明一個問題,開始時我已經說過了,程序時在release下運行的,為什么我們要給出這個條件呢?因為,在debug模式下,編譯器會延長局部變量的生命周期直至方法的結束,而release模式下,方法中的代碼下沒有再調用的變量生命周期都已結束,被認為可以回收的對象,明確這一點是十分重要的。
根據上面的闡述,你是不是已經認識到:第一個代碼片段的答案是【不確定幾個】,因為如果我們程序實例化了很多變量,導致進行了一次垃圾回收的工作,那么變量timer就會被釋放掉;而第二個代碼片段,是我寫出的垃圾回收的極端情況,它的答案應該是:只打印出一個1.
是不是感覺有點驚訝?!接下來,我們將共同解開CLR垃圾回收機制的神秘面紗
垃圾回收的算法比較
對於所有的托管系統來說,垃圾回收機制的算法一般包含兩種:
- 引用計數器算法
- 引用追蹤算法
我們先來討論【引用計算器算法】的優缺點。該算法是在每個對象的實例都有一個內存空間來存儲當前被多少對象引用,引用增加是就加1,超出變量作用域的就減一直至為0,就認為該對象可以被回收了,此種算法簡單有效,但它不能解決循環引用的情況,如果a引用了b,b再引用了a(a,b為兩個對象的實例),那么a和b永遠不會被釋放.
[引用追蹤算法]它只關心堆上的對象是否有變量引用它,如果沒有就認為是可以回收的對象。而CLR就是使用的這種垃圾回收算法,接下來,我們來共同學習一下這種算法在CLR中的應用
垃圾回收機制的步驟
一次垃圾回收一般分為三個步驟:
- 標記
- 回收
- 壓縮
標記
這一步的只要工作是找到堆上沒有被變量引用的對象實例。引用對象在分配內存時都加了一個區塊叫【同步塊索引】,該索引占64位,8個字節(64位系統上),對堆上的對象進行標記時就是用了這一塊區域的某一位。
- 在開始標記之前,先把堆上的所有對象的這一位標記為0。
- 堆上的對象有變量指向的,這一位改成1。這表示該對象時可達的
- 標記工作結束后,對象的【同步塊索引】那一位標記為0的,就代表時可以回收的對象
標記工作的模式
標記對象的工作有兩種模式:
- 同步 :標記工作開始之處,就暫停所有線程,開始標記工作
- 並發 :起一個低優先級的線程執行標記工作,直到找到有為0的對象,再暫停所有線程,進行垃圾回收工作
回收
回收工作就很簡單了,在堆上刪除掉標記為0 的對象
壓縮
對象被刪除后,會導致內存空間有碎片,這個時候CLR就會執行一次壓縮工作,將不連續的內存使用,變成連續的;壓縮后,變量的引用地址和堆上對象分配的空間地址不對應了,為了解決這個問題,CLR又執行了一次引用地址的偏移修改。之后再啟動所有被暫停的線程,一次垃圾回收就執行完畢了!
垃圾回收機制的優化
上一節講的垃圾回收機制有一個大的性能問題,它每次執行標記工作時都要掃描一遍堆上的所有對象,這是就產生了一個性能問題,微軟為了解決這個問題,提出了代的概念,首先他給出了一下假設:
- 對象越新,生存期越短
- 對象越老,生存期越長
- 回收堆的一部分,速度快於回收整個堆
三世同堂
CLR只支持最多3代的對象。0代、1代、2代
在CLR初始化時,CLR會對這三代回收對象各自預留一個空間,當每個代中的對象超出整個空間時,就會執行一次垃圾回收。CLR會根據程序執行情況動態的調整這三個預留空間的大小,這里我們不去了解這種動態調整的情況,接下來我們來說一下怎么產生的0、1、2代對象以及它們怎么被回收的
垃圾回收基於代的優化
- CLR初始化后,只有0代的對象
- 隨着應用程序的使用,堆上0代對象的內存空間超出了CLR為其預留的空間,就會進行一次垃圾回收
- 本次垃圾回收,留存下來的對象,會變成1代對象
- 循環執行2,3步驟,當1代對象達到預留空間時,CLR會進行1代和0代對象的垃圾回收
- 本次垃圾回收留存下來的1代對象,變成2代對象
- 循環執行2,3,4,5,當2代對象達到預留空間時,CLR會進行三代對象的垃圾回收
垃圾回收的其他知識點
- 應用程序可以強制對所有代的對象進行垃圾,需要使用 GC.Collect();Collect方法有5個重載
- 針對大對象(85000字節以上),CLR單獨在對上分配一塊內存區域,其對象總是2代對象,因此,我們應該確保大對象的生命周期應該很長,否則CLR頻繁對2代對象進行回收,會降低性能
- ~ClassName(),析構函數總是在垃圾回收后執行,因此存在析構函數的對象總會被留存到下一代進行垃圾回收