【轉】.NET內存管理、垃圾回收

1. Stack和Heap
    每一個線程對應一個stack,線程建立的時候CLR爲其建立這個stack,stack主要做用是記錄函數的執行狀況。值類型變量(函數的參數、局部變量 等非成員變量)都分配在stack中,引用類型的對象分配在heap中,在stack中保存heap對象的引用指針。GC只負責heap對象的釋 放,heap內存空間管理

Heap內存分配
    
    除去pinned object等影響,heap中的內存分配很簡單,一個指針記錄heap中分配的起始地址,根據對象大小連續的分配內存

Stack結構
    每一個函數調用時,邏輯上在thread stack中會產生一個幀(stack frame),函數返回時對應的stack frame被釋放掉
    用個簡單的函數查看執行時CLR對棧的處理狀況:算法

複製代碼
static void Main(string[] args)
{
    int r = Sum(2, 3, 4, 5, 6);
}
private static int Sum(int a, int b, int c, int d, int e)
{
    return a + b + c + d + e;
}
複製代碼

    JIT編譯後主要彙編代碼以下(其餘的狀況下彙編代碼可能有所差異,但用這個簡單函數大體看下棧的管理已經足夠):緩存

複製代碼
;====函數Main====
push    4         ;第3個參數到最後一個參數壓棧
push    5    
push    6    
mov    edx,3   ;第一、第2個參數分別放入ecx、edx寄存器
mov    ecx,2 
call    dword ptr ds:[00AD96B8h]  ;調用函數Sum,執行call的時候返回地址(即下面這條mov語句的地址)自動壓棧 了
mov    dword ptr [ebp-0Ch],eax   ;將函數返回值設置到局部變量r中(函數調用結束返回值在eax寄存器中)

;====函數Sum====
push    ebp           ;保存原始ebp寄存器
mov    ebp,esp     ;將當前棧指針保存在ebp中,後面使用ebp對參數和局部變量尋址
sub    esp,8         ;分配兩個局部變量
mov    dword ptr [ebp-4],ecx         ;第1個參數放入局部變量
mov    dword ptr [ebp-8],edx         ;第2個參數放入局部變量
......     ;CLR的檢查代碼
mov    eax,dword ptr [ebp-4]          ;a + b + c + d + e
add    eax,dword ptr [ebp-8]          ;第1個參數+第2個參數(2+3)
add    eax,dword ptr [ebp+10h]      ;+第3個參數(4)
add    eax,dword ptr [ebp+0Ch]      ;+第4個參數(5)
add    eax,dword ptr [ebp+8]          ;+第5個參數(6)
mov    esp,ebp    ;恢復棧指針(局部變量被釋放了)
pop    ebp          ;恢復原始的ebp寄存器值
ret    0Ch   ;函數返回. 1: 返回地址自動出 棧; 2: esp減去0Ch(12個字節),即從棧中清除調用參 數; 3: 返回值在eax寄存器中
複製代碼

    執行時刻的stack狀態以下(棧基地址爲高端地址,棧頂爲低端地址):
    
    Stack狀態變化過程:
    a). 調用者將第三、第四、第5個參數壓棧,第一、第2個參數分別放入ecx、edx寄存器
    b). call指令調用函數Sum,並自動將函數返回地址壓棧,代碼跳轉到函數Sum開始執行
    c). 函數Sum先將寄存器ebp壓棧保存,並將esp放入ebp,用於後面對參數和局部變量尋址
    d). 定義局部變量以及省略掉的是額外代碼,跟Sum函數業務無關
    e). 執行加法操做,結果保存在eax寄存器中
    f). 恢復esp寄存器,這樣函數Sum中全部的局部變量以及其餘壓棧操做所有釋放出來
    g). 原始ebp的值出棧,恢復ebp,這樣棧徹底恢復到進入Sum函數調用時的狀態
    h). ret指令執行函數返回,返回值在eax寄存器中,返回地址爲call指令壓棧的地址,返回地址自動出棧。0Ch指示處理器在函數返回時釋放棧中12個字 節,即由被調用者清除壓棧的參數。函數返回以後,本次Sum調用的棧分配所有釋放
    這種調用約定相似__fastcall

    結合引用類型變量、值類型的ref參數,下面代碼簡化的stack狀態以下:
    代碼:
服務器

複製代碼
public static void Run(int i)
{
    int j = 9;
    MyClass1 c = new MyClass1();
    c.x = 8;
    int result = Sum(i, 5, ref j, c);
}

public static int Sum(int a, int b, ref int c, MyClass1 obj)
{
    int r = a + b + c + obj.x;
    return r;
}

public class MyClass1
{
    public int x;
}
複製代碼

    Stack狀態:
    
    任什麼時候候引用類型都分配在heap中,在stack中只是保存對象的引用地址。Run函數執行完畢以後,heap中的MyClass1對象c成爲可回收的垃圾對象,在GC時進行回收

2. Mark-Compact 標記壓縮算法
    簡單把.NET的GC算法看做Mark-Compact算法
    階段1: Mark-Sweep 標記清除階段
    先假設heap中全部對象均可以回收,而後找出不能回收的對象,給這些對象打上標記,最後heap中沒有打標記的對象都是能夠被回收的
    階段2: Compact 壓縮階段
    對象回收以後heap內存空間變得不連續,在heap中移動這些對象,使他們從新從heap基地址開始連續排列,相似於磁盤空間的碎片整理
    
    Heap內存通過回收、壓縮以後,能夠繼續採用前面的heap內存分配方法,即僅用一個指針記錄heap分配的起始地址就能夠

    主要處理步驟:將線程掛起=>肯定roots=>建立reachable objects graph=>對象回收=>heap壓縮=>指針修復
    能夠這樣理解roots:heap中對象的引用關係錯綜複雜(交叉引用、循環引用),造成複雜的graph,roots是CLR在heap以外能夠找到的 各類入口點。GC搜索roots的地方包括全局對象、靜態變量、局部對象、函數調用參數、當前CPU寄存器中的對象指針(還有finalization queue)等。主要能夠歸爲2種類型:已經初始化了的靜態變量、線程仍在使用的對象(stack+CPU register)
    Reachable objects:指根據對象引用關係,從roots出發能夠到達的對象。例如當前執行函數的局部變量對象A是一個root object,他的成員變量引用了對象B,則B是一個reachable object。從roots出發能夠建立reachable objects graph,剩餘對象即爲unreachable,能夠被回收
    
    指針修復是由於compact過程移動了heap對象,對象地址發生變化,須要修復全部引用指針,包括stack、CPU register中的指針以及heap中其餘對象的引用指針
    Debug和release執行模式之間稍有區別,release模式下後續代碼沒有引用的對象是unreachable的,而debug模式下須要等到 當前函數執行完畢,這些對象纔會成爲unreachable,目的是爲了調試時跟蹤局部對象的內容
    傳給了COM+的託管對象也會成爲root,而且具備一個引用計數器以兼容COM+的內存管理機制,引用計數器爲0時這些對象纔可能成爲被回收對象
    Pinned objects指分配以後不能移動位置的對象,例如傳遞給非託管代碼的對象(或者使用了fixed關鍵字),GC在指針修復時沒法修改非託管代碼中的引用 指針,所以將這些對象移動將發生異常。pinned objects會致使heap出現碎片,但大部分狀況來講傳給非託管代碼的對象應當在GC時可以被回收掉

3. Generational 分代算法
    程序可能使用幾百M、幾G的內存,對這樣的內存區域進行GC操做成本很高,分代算法具有必定統計學基礎,對GC的性能改善效果比較明顯
    將對象按照生命週期分紅新的、老的,根據統計分佈規律所反映的結果,能夠對新、老區域採用不一樣的回收策略和算法,增強對新區域的回收處理力度,爭取在較短 時間間隔、較小的內存區域內,以較低成本將執行路徑上大量新近拋棄再也不使用的局部對象及時回收掉
    分代算法的假設前提條件:
    a). 大量新建立的對象生命週期都比較短,而較老的對象生命週期會更長
    b). 對部份內存進行回收比基於所有內存的回收操做要快
    c). 新建立的對象之間關聯程度一般較強。heap分配的對象是連續的,關聯度較強有利於提升CPU cache的命中率

    .NET將heap分紅3個代齡區域: Gen 0、Gen 一、Gen 2
    
    Heap分爲3個代齡區域,相應的GC有3種方式: # Gen 0 collections, # Gen 1 collections, # Gen 2 collections。若是Gen 0 heap內存達到閥值,則觸發0代GC,0代GC後Gen 0中倖存的對象進入Gen 1。若是Gen 1的內存達到閥值,則進行1代GC,1代GC將Gen 0 heap和Gen 1 heap一塊兒進行回收,倖存的對象進入Gen 2。2代GC將Gen 0 heap、Gen 1 heap和Gen 2 heap一塊兒回收
    Gen 0和Gen 1比較小,這兩個代齡加起來老是保持在16M左右;Gen 2的大小由應用程序肯定,可能達到幾G,所以0代和1代GC的成本很是低,2代GC稱爲full GC,一般成本很高。粗略的計算0代和1代GC應當能在幾毫秒到幾十毫秒之間完成,Gen 2 heap比較大時full GC可能須要花費幾秒時間。大體上來說.NET應用運行期間2代、1代和0代GC的頻率應當大體爲1:10:100
    
    圖爲一個ASP.NET程序運行的Performance Moniter,Gen 0 heap size(紅色)平均6M,Gen 1(藍色)平均5M,Gen 2(黃色)達到620M,Gen 0+Gen 1平均13.2M,最大19.8M

    直觀上來看,程序的運行由一系列函數調用組成,函數運行期間會建立不少局部對象,函數結束以後也就產生大量待回收的對象。採用分代算法增強較新代齡的垃圾 回收力度,一般可以極大的提升垃圾回收效率,不然就是極特殊的程序,或者是不合理的對象關聯設計。例如ASP.NET程序,應當確保絕大部分用於HTTP 請求處理的對象在0代和1代垃圾回收中被釋放掉

    爲heap記錄幾個指針能夠肯定代齡區域範圍,建立reachable objects graph時根據對象的地址能夠肯定對象位於哪一個代齡區域,0代GC在建立graph時若是遇到1代、2代heap對象,能夠直接越過不用繼續遍歷下去, 較老代齡的對象若是引用了較新代齡的對象,能夠經過Win32 API GetWriteWatch訂閱內存更新通知,記錄在"card table"中,輔助較低代齡的GC正確構造graph

4. LOH
    .NET 1.1和2.0中,85000字節如下的對象稱爲小對象,分配在Gen 0 heap中,85000字節以上的對象稱爲大對象,分配在Large Object Heap中,這是由於GC在heap壓縮時移動大的內存塊須要消耗大量CPU時間,經過性能調優實踐肯定了85000字節這樣一個閥值
    LOH只在2代GC時進行回收,採用Mark-Sweep算法,沒有壓縮處理,所以LOH中的內存分配是不連續的,使用一個空閒列表free list記錄LOH中的空閒空間,對釋放出來的空間進行管理
    
    上圖中obj一、obj2釋放以後,其空間合併起來成爲free list的一個節點,隨後被分配給obj4

    何時觸發垃圾回收?
    前面已經提到,0代和1代垃圾回收主要由閥值控制。初始時Gen 0 heap大小與CPU緩存的大小相關,運行時CLR根據內存請求狀態動態調整Gen 0 heap大小,但Gen 0和Gen 1總大小保持在16M左右
Gen 2 heap和LOH都在full GC時進行回收,full GC主要由2類事件觸發:
    a). 進入Gen 2 heap和LOH的對象不少,超過了必定比例。RegisterForFullGCNotification的參數 maxGenerationThreshold、largeObjectHeapThreshold能夠分別爲Gen 2 heap和LOH設定這個值
    b). 操做系統內存吃緊的時候。CLR會接收到操做系統內存緊張的通知消息,觸發full GC

5. Heap細節、擴容與收縮
    Heap的代齡是邏輯上的結構,heap實際內存申請和分配以及釋放以segment(段)爲單位,workstation GC模式segment大小爲16M,server GC模式segment大小爲64M。Gen 0和Gen 1 heap老是位於同一個段中,叫作ephemeral segment(新生段),所以max(Gen 0 heap size+Gen 1 heap size)≈16M || 64M,Gen 2 heap由0個或多個segments組成,LOH由1個或多個segments組成
    .NET程序啓動時CLR爲heap建立2個segment,一個做爲ephemeral segment,另外一個用於LOH。.NET使用VirtualAlloc申請和分配heap內存,在LOH中分配新對象時沒有足夠的空間,或者1代GC 時進入Gen 2的對象過多空間不夠,.NET將爲LOH或者小對象heap分配新的segment。申請新的segment失敗將由EE拋出OutOfMemory異 常
    Full GC後徹底空閒的segments將被釋放掉,內存返回給操做系統

    .NET 2.0對GC的一個重要改進是儘可能改善heap碎片處理。heap碎片主要由pinned objects引發,改善措施主要有2個方面。首先是延遲升級,若是ephemeral segment存在pinned objects,則儘量的延遲他們升級到Gen 2的時間點,考慮pinned objects的同時儘可能充分利用當前ephemeral segment的空間;其次是重複利用Gen 2的空間,若是Gen 2中存在pinned objects的segments釋放出了足夠空間,該segments可能從新做爲ephemeral segment使用

6. GC方式
    有Workstation GC with Concurrent GC off、 Workstation GC with Concurrent GC on、Server GC 3種
    Workstation GC with Concurrent GC off: 用於單CPU機器實現高吞吐量,採用一系列策略觀察內存分配以及每次GC的情況,動態調整GC策略,儘量使程序隨着運行時狀態的變化實現高效的GC操 做,但進行GC時會凍結全部線程
    Workstation GC with Concurrent GC on: 用於響應時間很是重要的交互式程序,例如流媒體的播放等(若是一次full GC致使應用程序中斷幾秒、十幾秒時間,用戶將沒法忍受)。這種方式利用多CPU對full GC進行並行處理,不是整個full GC期間凍結全部線程,而是將full GC切分紅屢次很短的時間對線程進行凍結,在線程凍結時間以外,應用程序仍然能夠正常運行,進行內存分配,這主要經過將Gen 0 heap size設置的比non-concurrent GC大不少而實現,使得GC操做時線程仍然可以在Gen 0 heap中進行內存分配,但若是Gen 0 heap用完後GC仍然沒有結束,線程仍然會出現阻塞。這種方式付出的代價是working set和GC所需時間比non-concurrent GC要大一些
    Server GC: 用於多CPU機器的服務器應用程序實現高吞吐量和伸縮性,充分利用服務器的大內存。.NET爲每一個CPU建立一組heap(包括Gen 0, 1, 2和LOH)和一個GC線程,每一個CPU能夠獨立的爲相應的heap執行GC操做,而其餘CPU則正常執行處理。最佳的應用場景是多線程之間內存結構基本 相同,執行的工做相同或相似

    單CPU機器上只能使用workstation GC,默認狀況下爲Workstation GC with Concurrent GC on方式,單CPU機器上配置爲Server GC無效,仍然使用workstation GC;多CPU服務器上的ASP.NET默認使用Server GC方式,Server GC時不能使用concurrent方式
    concurrent GC能夠用於單CPU機器,它與CPU數量無關
    對於ASP.NET程序應當儘可能保證一個CPU僅對應一個GC線程,防止同一個CPU上面多個GC線程之間的衝突形成性能問題。若是使用了Web Garden則應當使用Workstation GC with Concurrent GC off。Web Garden爲了提升吞吐量會致使多出幾倍的內存使用,每一個work process的內存有不少重複部分,Web Garden的最佳應用場景是多個進程之間使用一個共享的resource pool,避免內存的重複並儘量的提升吞吐量。在這一點上Server GC應當與Web Garden相似,但Web Garden在多個進程中,而Server GC是在同一個進程中經過多線程實現,目前沒有發現Server GC方面深刻一些的資料,不少東西只能根據現有資料作一些猜測
    爲workstation GC禁用concurrent GC:
多線程

<configuration>
    <runtime>
        <gcConcurrent enabled="false"/>
    </runtime>
</configuration> 

    啓用Server GC:
異步

<configuration>
    <runtime>
        <gcServer enabled=「true"/>
    </runtime>
</configuration>


7. Finalization
    具備finalize method的對象在垃圾回收時,.NET先調用finalize method,而後再進行回收,具體處理以下:
    a). 在heap建立具備finalize method的對象時,對象指針會放入finalization queue;
    b). 垃圾回收時,具備finalize method的對象若是成爲unreachable,則將其指針從finalization queue中移除,放入freachable queue,在本次垃圾回收處理中並不對這些對象進行回收;其它沒有finalize method的unreachable對象正常回收。freachable queue中的對象是reachable的(它引用到的其餘對象也都是reachable的)
    c). 垃圾回收結束後,若是freachable queue非空,則一個專門的運行時線程finalizer thread被喚醒,它逐個調用freachable queue中對象的finalize method,而後將其指針從freachable queue中移除
    d). 通過步驟c的處理以後,第二次垃圾回收時這些對象就成爲unreachable,被正常回收
    由於finalize method被設計用於非託管資源的釋放,對這些資源的釋放可能須要較長的時間,爲了優化垃圾回收處理的性能,所以將調用finalize method專門交給一個獨立的線程finalizer thread異步進行處理,這樣也形成finalize method的對象須要通過2次垃圾回收處理

參考:
Garbage Collection - Past, Present and Future, Patrick Dussud, 中文翻譯: .NET垃圾收集器的過去如今和將來(一)(二)
C# Heap(ing) Vs Stack(ing) in .NET Part IPart IIPart IIIPart IV Matthew Cochran
Garbage Collection: Automatic Memory Management in the Microsoft .NET Framework Jeffrey Richter
Garbage Collection Part 2: Automatic Memory Management in the Microsoft .NET Framework Jeffrey Richter
CLR Inside Out: Large Object Heap Uncovered Maoni Stephens
Heap: Pleasures and Pains Murali R. Krishnan
The Dangers of the Large Object Heap Andrew Hunter
Garbage Collection Notifications
Garbage Collector Basics and Performance Hints Rico Mariani
CLR Inside Out: Investigating Memory Issues Claudio Caldato and Maoni Stephens
Understanding Garbage Collection in .NET Andrew Hunter
Using GC Efficiently Part 1Part 2Part 3Part 4 Maoni Stephens
Notes on the CLR Garbage Collector Vineet Gupta
The Mystery of Concurrent GC Mark Smith
Garbage Collection Curriculum Ferreira Paulo, Veiga Luís
Java theory and practice: A brief history of garbage collection Brian Goetz
ide

相關文章
相關標籤/搜索