日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

CoreCLR源码探索(三) GC内存分配器的内部实现

發(fā)布時(shí)間:2023/12/4 编程问答 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 CoreCLR源码探索(三) GC内存分配器的内部实现 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

在前一篇中我講解了new是怎么工作的, 但是卻一筆跳過了內(nèi)存分配相關(guān)的部分.
在這一篇中我將詳細(xì)講解GC內(nèi)存分配器的內(nèi)部實(shí)現(xiàn).
在看這一篇之前請必須先看完微軟BOTR文檔中的"Garbage Collection Design",
原文地址是: https://github.com/dotnet/coreclr/blob/master/Documentation/botr/garbage-collection.md
譯文可以看知平軟件的譯文或我后來的譯文
請務(wù)必先看完"Garbage Collection Design", 否則以下內(nèi)容你很可能會無法理解

服務(wù)器GC和工作站GC

關(guān)于服務(wù)器GC和工作站GC的區(qū)別, 網(wǎng)上已經(jīng)有很多資料講解這篇就不再說明了.
我們來看服務(wù)器GC和工作站GC的代碼是怎么區(qū)別開來的.
默認(rèn)編譯CoreCLR會對同一份代碼以使用服務(wù)器GC還是工作站GC的區(qū)別編譯兩次, 分別在SVR和WKS命名空間中:

源代碼: https://github.com/dotnet/coreclr/blob/release/1.1.0/src/gc/gcsvr.cpp

#define SERVER_GC 1namespace SVR { #include "gcimpl.h"#include "gc.cpp"}

源代碼: https://github.com/dotnet/coreclr/blob/release/1.1.0/src/gc/gcwks.cpp

#ifdef SERVER_GC#undef SERVER_GC#endifnamespace WKS { #include "gcimpl.h"#include "gc.cpp"}

當(dāng)定義了SERVER_GC時(shí), MULTIPLE_HEAPS和會被同時(shí)定義.
定義了MULTIPLE_HEAPS會使用多個(gè)堆(Heap), 服務(wù)器GC每個(gè)cpu核心都會對應(yīng)一個(gè)堆(默認(rèn)), 工作站GC則全局使用同一個(gè)堆.

源代碼: https://github.com/dotnet/coreclr/blob/release/1.1.0/src/gc/gcimpl.h

#ifdef SERVER_GC#define MULTIPLE_HEAPS 1#endif // SERVER_GC

后臺GC無論是服務(wù)器GC還是工作站GC都會默認(rèn)支持, 但運(yùn)行時(shí)不一定會啟用.

源代碼: https://github.com/dotnet/coreclr/blob/release/1.1.0/src/gc/gcpriv.h

#define BACKGROUND_GC //concurrent background GC (requires WRITE_WATCH)

我們從https://www.microsoft.com/net下回來的CoreCLR安裝包中已經(jīng)包含了服務(wù)器GC和后臺GC的支持,但默認(rèn)不會開啟.
開啟它們可以修改project.json中的·runtimeOptions·節(jié), 例子如下:

{"runtimeOptions": {"configProperties": {"System.GC.Server": true,"System.GC.Concurrent": true}}}

設(shè)置后發(fā)布項(xiàng)目可以看到coreapp.runtimeconfig.json, 運(yùn)行時(shí)會只看這個(gè)文件.
微軟官方的文檔: https://docs.microsoft.com/en-us/dotnet/articles/core/tools/project-json

GC相關(guān)的類和它們的關(guān)系

我先用兩張圖來解釋服務(wù)器GC和工作站GC下GC相關(guān)的類的關(guān)系

圖中一共有5個(gè)類型

  • GCHeap

    • 實(shí)現(xiàn)了IGCHeap接口, 公開GC層的接口給EE(運(yùn)行引擎)層調(diào)用

    • 在工作站GC下只有一個(gè)實(shí)例, 不會關(guān)聯(lián)gc_heap對象, 因?yàn)楣ぷ髡綠C下gc_heap的所有成員都會被定義為靜態(tài)變量

    • 在服務(wù)器GC下有1+cpu核心數(shù)個(gè)實(shí)例(默認(rèn)), 第一個(gè)實(shí)例用于當(dāng)接口, 其它對應(yīng)cpu核心的實(shí)例都會各關(guān)聯(lián)一個(gè)gc_heap實(shí)例

  • gc_heap

    • 內(nèi)部的使用的堆類型, 用于負(fù)責(zé)內(nèi)存的分配和回收

    • 在工作站GC下無實(shí)例, 所有成員都會定義為靜態(tài)變量

    • 在工作站GC下generation_table這個(gè)成員不會被定義, 而是使用全局變量generation_table

    • 在服務(wù)器GC下有cpu核心數(shù)個(gè)實(shí)例(默認(rèn)), 各關(guān)聯(lián)一個(gè)GCHeap實(shí)例

  • generation

    • 儲存各個(gè)代的信息, 例如地址范圍和使用的段

    • 儲存在generation_table中, 一個(gè)generation_table包含了5個(gè)generation, 前面的是0 1 2 3代, 最后一個(gè)不會被初始化和使用

    • 在工作站GC下只有1個(gè)generation_table, 就是全局變量generation_table

    • 在服務(wù)器GC下generation_table是gc_heap的成員, 有多少個(gè)gc_heap就有多少個(gè)generation_table

  • heap_segment

    • 堆段, 供分配器使用的一段內(nèi)存, 用鏈表形式保存

    • 每個(gè)gc_heap中都有一個(gè)或一個(gè)以上的segment

    • 每個(gè)gc_heap中都有一個(gè)ephemeral heap segment(用于存放最年輕對象)

    • 每個(gè)gc_heap中都有一個(gè)large heap segment(用于存放大對象)

    • 在工作站GC下segment的默認(rèn)大小是256M(0x10000000字節(jié))

    • 在服務(wù)器GC下segment的默認(rèn)大小是4G(0x100000000字節(jié))

  • alloc_context

    • 分配上下文, 指向segment中的一個(gè)范圍, 用于實(shí)際分配對象

    • 每個(gè)線程都有自己的分配上下文, 因?yàn)橹赶虻姆秶灰粯铀灾灰?dāng)前范圍還有足夠空間, 分配對象時(shí)不需要線程鎖

    • 分配上下文的默認(rèn)范圍是8K, 也叫分配單位(Allocation Quantum)

    • 分配小對象時(shí)會從這8K中分配, 分配大對象時(shí)則會直接從段(segment)中分配

    • 代0(gen 0)還有一個(gè)默認(rèn)的分配上下文供內(nèi)部使用, 和線程無關(guān)

GCHeap的源代碼摘要:

GCHeap的定義: https://github.com/dotnet/coreclr/blob/release/1.1.0/src/gc/gcimpl.h#L61
全局的GCHeap實(shí)例: https://github.com/dotnet/coreclr/blob/release/1.1.0/src/gc/gc.h#L105

這里是1.1.0的代碼, 1.2.0全局GCHeap會分別保存到gcheaputilities.h(g_pGCHeap)和gc.cpp(g_theGCHeap), 兩處地方都指向同一個(gè)實(shí)例.

// 相當(dāng)于extern GCHeap* g_pGCHeap;GPTR_DECL(GCHeap, g_pGCHeap);

gc_heap的源代碼摘要:

gc_heap的定義: https://github.com/dotnet/coreclr/blob/release/1.1.0/src/gc/gcpriv.h#L1079
這個(gè)類有300多個(gè)成員(從ephemeral_low開始),

generation的源代碼摘要:

generation的定義: https://github.com/dotnet/coreclr/blob/release/1.1.0/src/gc/gcpriv.h#L754
這里我只列出這篇文章涉及到的成員

class generation {public: ? ?// 默認(rèn)的分配上下文alloc_context ? allocation_context; ? ?// 用于分配的最新的堆段heap_segment* ? allocation_segment; ? ?// 開始的堆段PTR_heap_segment start_segment; ? ?// 用于區(qū)分對象在哪個(gè)代的指針, 在此之后的對象都屬于這個(gè)代, 或比這個(gè)代更年輕的代uint8_t* ? ? ? ?allocation_start; ? ?// 用于儲存和分配自由對象(Free Object, 又名Unused Array, 可以理解為碎片空間)的分配器allocator ? ? ? free_list_allocator; ? ?// 這個(gè)代是第幾代int gen_num; };

heap_segment的源代碼摘要:

heap_segment的定義: https://github.com/dotnet/coreclr/blob/release/1.1.0/src/gc/gcpriv.h#L4166
這里我只列出這篇文章涉及到的成員

class heap_segment {public: ? ?// 已實(shí)際分配地址 (mem + 已分配大小)// 更新有可能會延遲uint8_t* ? ? ? ?allocated; ? ?// 已提交到物理內(nèi)存的地址 (this + SEGMENT_INITIAL_COMMIT)uint8_t* ? ? ? ?committed; ? ?// 預(yù)留到的分配地址 (this + size)uint8_t* ? ? ? ?reserved; ? ?// 已使用地址 (mem + 已分配大小 - 對象頭大小)uint8_t* ? ? ? ?used; ? ?// 初始分配地址 (服務(wù)器gc開啟時(shí): this + OS_PAGE_SIZE, 否則: this + sizeof(*this) + alignment)uint8_t* ? ? ? ?mem; ? ?// 下一個(gè)堆段PTR_heap_segment next; ? ?// 屬于的gc_heap實(shí)例gc_heap* ? ? ? ?heap; };

alloc_context的源代碼摘要:

alloc_context的定義: https://github.com/dotnet/coreclr/blob/release/1.1.0/src/gc/gc.h#L162
這里是1.1.0的代碼, 1.2.0這些成員移動到了gcinterface.h的gc_alloc_context, 但是成員還是一樣的

struct alloc_context { ? ?// 下一次分配對象的開始地址uint8_t* ? ? ? alloc_ptr; ? ?// 可以分配到的最終地址uint8_t* ? ? ? alloc_limit; ? ?// 歷史分配的小對象大小合計(jì)int64_t ? ? ? ?alloc_bytes; //Number of bytes allocated on SOH by this context// 歷史分配的大對象大小合計(jì)int64_t ? ? ? ?alloc_bytes_loh; //Number of bytes allocated on LOH by this context#if defined(FEATURE_SVR_GC)// 空間不夠需要獲取更多空間時(shí)使用的GCHeap// 分alloc_heap和home_heap的作用是平衡各個(gè)heap的使用量,這樣并行回收時(shí)可以減少處理各個(gè)heap的時(shí)間差異SVR::GCHeap* ? alloc_heap; ? ?// 原來的GCHeapSVR::GCHeap* ? home_heap;#endif // defined(FEATURE_SVR_GC)// 歷史分配對象次數(shù)int ? ? ? ? ? ?alloc_count; };

堆段的物理結(jié)構(gòu)

為了更好理解下面即將講解的代碼,請先看這兩張圖片

分配對象內(nèi)存的代碼流程

還記得上篇我提到過的AllocateObject函數(shù)嗎? 這個(gè)函數(shù)由JIT_New調(diào)用, 負(fù)責(zé)分配一個(gè)普通的對象.
讓我們來繼續(xù)跟蹤這個(gè)函數(shù)的內(nèi)部吧:

AllocateObject函數(shù)的內(nèi)容: https://github.com/dotnet/coreclr/blob/release/1.1.0/src/vm/gchelpers.cpp#L931
AllocateObject的其他版本同樣也會調(diào)用AllocAlign8或Alloc函數(shù), 下面就不再貼出其他版本的函數(shù)代碼了.

Alloc函數(shù)的內(nèi)容: https://github.com/dotnet/coreclr/blob/release/1.1.0/src/vm/gchelpers.cpp#L931

GetGCHeap函數(shù)的內(nèi)容: https://github.com/dotnet/coreclr/blob/release/1.1.0/src/gc/gc.h#L377

static GCHeap *GetGCHeap(){LIMITED_METHOD_CONTRACT; ? ?// 返回全局的GCHeap實(shí)例// 注意這個(gè)實(shí)例只作為接口使用,不和具體的gc_heap實(shí)例關(guān)聯(lián)_ASSERTE(g_pGCHeap != NULL); ? ?return g_pGCHeap; }

GetThreadAllocContext函數(shù)的內(nèi)容: https://github.com/dotnet/coreclr/blob/release/1.1.0/src/vm/gchelpers.cpp#L54

inline alloc_context* GetThreadAllocContext(){WRAPPER_NO_CONTRACT;assert(GCHeap::UseAllocationContexts()); ?
?// 獲取當(dāng)前線程并返回m_alloc_context成員的地址return & GetThread()->m_alloc_context; }

GCHeap::Alloc函數(shù)的內(nèi)容: https://raw.githubusercontent.com/dotnet/coreclr/release/1.1.0/src/gc/gc.cpp

分配小對象內(nèi)存的代碼流程

讓我們來看一下小對象的內(nèi)存是如何分配的

allocate函數(shù)的內(nèi)容: https://raw.githubusercontent.com/dotnet/coreclr/release/1.1.0/src/gc/gc.cpp
這個(gè)函數(shù)嘗試從分配上下文分配內(nèi)存, 失敗時(shí)調(diào)用allocate_more_space為分配上下文指定新的空間
這里的前半部分的處理還有匯編版本, 可以看上一篇分析的JIT_TrialAllocSFastMP_InlineGetThread函數(shù)

allocate_more_space函數(shù)的內(nèi)容: https://raw.githubusercontent.com/dotnet/coreclr/release/1.1.0/src/gc/gc.cpp
這個(gè)函數(shù)會在有多個(gè)heap時(shí)調(diào)用balance_heaps平衡各個(gè)heap的使用量, 然后再調(diào)用try_allocate_more_space函數(shù)


try_allocate_more_space函數(shù)的內(nèi)容: https://raw.githubusercontent.com/dotnet/coreclr/release/1.1.0/src/gc/gc.cpp
這個(gè)函數(shù)會獲取MSL鎖, 檢查是否有必要觸發(fā)GC, 然后根據(jù)gen_number參數(shù)調(diào)用allocate_small或allocate_large函數(shù)

allocate_small函數(shù)的內(nèi)容: https://raw.githubusercontent.com/dotnet/coreclr/release/1.1.0/src/gc/gc.cpp
循環(huán)嘗試進(jìn)行各種回收內(nèi)存的處理和調(diào)用soh_try_fit函數(shù), soh_try_fit函數(shù)分配成功或手段已經(jīng)用盡時(shí)跳出循環(huán)


soh_try_fit函數(shù)的內(nèi)容: https://raw.githubusercontent.com/dotnet/coreclr/release/1.1.0/src/gc/gc.cpp
這個(gè)函數(shù)會先嘗試調(diào)用a_fit_free_list_p從自由對象列表中分配, 然后嘗試調(diào)用a_fit_segment_end_p從堆段結(jié)尾分配


a_fit_free_list_p函數(shù)的內(nèi)容: https://raw.githubusercontent.com/dotnet/coreclr/release/1.1.0/src/gc/gc.cpp
這個(gè)函數(shù)會嘗試從自由對象列表中找到足夠大小的空間, 如果找到則把分配上下文指向這個(gè)空間

a_fit_segment_end_p函數(shù)的內(nèi)容: https://raw.githubusercontent.com/dotnet/coreclr/release/1.1.0/src/gc/gc.cpp
這個(gè)函數(shù)會嘗試在堆段的結(jié)尾找到一塊足夠大小的空間, 如果找到則把分配上下文指向這個(gè)空間

adjust_limit_clr函數(shù)的內(nèi)容: https://raw.githubusercontent.com/dotnet/coreclr/release/1.1.0/src/gc/gc.cpp
這個(gè)函數(shù)會給分配上下文設(shè)置新的范圍
不管是從自由列表還是堆段的結(jié)尾分配都會調(diào)用這個(gè)函數(shù), 從自由列表分配時(shí)seg參數(shù)會是nullptr
調(diào)用完這個(gè)函數(shù)以后分配上下文就有足夠的空間了, 回到gc_heap::allocate的retry就可以成功的分配到對象的內(nèi)存

總結(jié)小對象內(nèi)存的代碼流程

  • allocate: 嘗試從分配上下文分配內(nèi)存, 失敗時(shí)調(diào)用allocate_more_space為分配上下文指定新的空間

    • try_allocate_more_space: 檢查是否有必要觸發(fā)GC, 然后根據(jù)gen_number參數(shù)調(diào)用allocate_small或allocate_large函數(shù)

    • soh_try_fit: 先嘗試調(diào)用a_fit_free_list_p從自由對象列表中分配, 然后嘗試調(diào)用a_fit_segment_end_p從堆段結(jié)尾分配

    • adjust_limit_clr: 給分配上下文設(shè)置新的范圍

    • adjust_limit_clr: 給分配上下文設(shè)置新的范圍

    • a_fit_free_list_p: 嘗試從自由對象列表中找到足夠大小的空間, 如果找到則把分配上下文指向這個(gè)空間

    • a_fit_segment_end_p: 嘗試在堆段的結(jié)尾找到一塊足夠大小的空間, 如果找到則把分配上下文指向這個(gè)空間

    • allocate_small: 循環(huán)嘗試進(jìn)行各種回收內(nèi)存的處理和調(diào)用soh_try_fit函數(shù)

    • allocate_more_space: 調(diào)用try_allocate_more_space函數(shù)

分配大對象內(nèi)存的代碼流程

讓我們來看一下大對象的內(nèi)存是如何分配的
分配小對象我們從gc_heap::allocate開始跟蹤, 這里我們從gc_heap::allocate_large_object開始跟蹤

allocate_large_object函數(shù)的內(nèi)容: https://raw.githubusercontent.com/dotnet/coreclr/release/1.1.0/src/gc/gc.cpp
這個(gè)函數(shù)和allocate函數(shù)不同的是它不會嘗試從分配上下文中分配, 而是直接從堆段中分配

allocate_more_space這個(gè)函數(shù)我們在之前已經(jīng)看過了, 忘掉的可以向前翻
這個(gè)函數(shù)會調(diào)用try_allocate_more_space函數(shù)
try_allocate_more_space函數(shù)在分配大對象時(shí)會調(diào)用allocate_large函數(shù)

allocate_large函數(shù)的內(nèi)容: https://raw.githubusercontent.com/dotnet/coreclr/release/1.1.0/src/gc/gc.cpp
這個(gè)函數(shù)的結(jié)構(gòu)和alloc_small相似但是內(nèi)部處理的細(xì)節(jié)不一樣

loh_try_fit函數(shù)的內(nèi)容: https://raw.githubusercontent.com/dotnet/coreclr/release/1.1.0/src/gc/gc.cpp
處理和soh_try_fit差不多, 先嘗試調(diào)用a_fit_free_list_large_p從自由對象列表中分配, 然后嘗試調(diào)用loh_a_fit_segment_end_p從堆段結(jié)尾分配


a_fit_free_list_large_p函數(shù)的內(nèi)容: https://raw.githubusercontent.com/dotnet/coreclr/release/1.1.0/src/gc/gc.cpp
和a_fit_free_list_p的處理基本相同, 但是在支持LOH壓縮時(shí)會生成填充對象, 并且有可能會調(diào)用bgc_loh_alloc_clr函數(shù)

adjust_limit_clr這個(gè)函數(shù)我們在看小對象的代碼流程時(shí)已經(jīng)看過
這里看bgc_loh_alloc_clr函數(shù)的內(nèi)容: https://raw.githubusercontent.com/dotnet/coreclr/release/1.1.0/src/gc/gc.cpp
這個(gè)函數(shù)是在后臺GC運(yùn)行時(shí)分配大對象使用的, 需要照顧到運(yùn)行中的后臺GC


loh_a_fit_segment_end_p函數(shù)的內(nèi)容: https://raw.githubusercontent.com/dotnet/coreclr/release/1.1.0/src/gc/gc.cpp
這個(gè)函數(shù)會遍歷第3代的堆段鏈表逐個(gè)調(diào)用a_fit_segment_end_p函數(shù)嘗試分配

總結(jié)大對象內(nèi)存的代碼流程

  • allocate_large_object: 調(diào)用allocate_more_space為一個(gè)空的分配上下文指定新的空間, 空間大小會等于對象的大小

    • try_allocate_more_space: 檢查是否有必要觸發(fā)GC, 然后根據(jù)gen_number參數(shù)調(diào)用allocate_small或allocate_large函數(shù)

    • loh_try_fit: 先嘗試調(diào)用a_fit_free_list_large_p從自由對象列表中分配, 然后嘗試調(diào)用loh_a_fit_segment_end_p從堆段結(jié)尾分配

    • a_fit_segment_end_p: 嘗試在堆段的結(jié)尾找到一塊足夠大小的空間, 如果找到則把分配上下文指向這個(gè)空間

    • bgc_loh_alloc_clr: 給分配上下文設(shè)置新的范圍, 照顧到后臺GC

    • adjust_limit_clr: 給分配上下文設(shè)置新的范圍

    • bgc_loh_alloc_clr: 給分配上下文設(shè)置新的范圍, 照顧到后臺GC

    • adjust_limit_clr: 給分配上下文設(shè)置新的范圍

    • a_fit_free_list_large_p: 嘗試從自由對象列表中找到足夠大小的空間, 如果找到則把分配上下文指向這個(gè)空間

    • loh_a_fit_segment_end_p: 遍歷第3代的堆段鏈表逐個(gè)調(diào)用a_fit_segment_end_p函數(shù)嘗試分配

    • allocate_large: 循環(huán)嘗試進(jìn)行各種回收內(nèi)存的處理和調(diào)用soh_try_fit函數(shù)

    • allocate_more_space: 調(diào)用try_allocate_more_space函數(shù)

CoreCLR如何管理系統(tǒng)內(nèi)存 (windows, linux)

看到這里我們應(yīng)該知道分配上下文, 小對象, 大對象的內(nèi)存都是來源于堆段, 那堆段的內(nèi)存來源于哪里呢?
GC在程序啟動時(shí)會創(chuàng)建默認(rèn)的堆段, 調(diào)用流程是init_gc_heap => get_initial_segment => make_heap_segment
如果默認(rèn)的堆段不夠用會創(chuàng)建新的堆段
小對象的堆段會通過gc1 => plan_phase => soh_get_segment_to_expand => get_segment => make_heap_segment創(chuàng)建
大對象的堆段會通過allocate_large => loh_get_new_seg => get_large_segment => get_segment_for_loh => get_segment => make_heap_segment創(chuàng)建

默認(rèn)的堆段會通過next_initial_memory分配內(nèi)存, 這一塊內(nèi)存在程序啟動時(shí)從reserve_initial_memory函數(shù)申請
reserve_initial_memory函數(shù)和make_heap_segment函數(shù)都會調(diào)用virtual_alloc函數(shù)

因?yàn)檎{(diào)用流程很長我這里就不一個(gè)個(gè)函數(shù)貼代碼了, 有興趣的可以自己去跟蹤
virtual_alloc函數(shù)的調(diào)用流程是

virtual_alloc => GCToOSInterface::VirtualReserve => ClrVirtualAllocAligned => ClrVirtualAlloc => CExecutionEngine::ClrVirtualAlloc => EEVirtualAlloc => VirtualAlloc

如果是windows, VirtualAlloc就是同名的windows api
如果是linux或者macosx, 調(diào)用流程是VirtualAlloc => VIRTUALReserveMemory => ReserveVirtualMemory
ReserveVirtualMemory函數(shù)會調(diào)用mmap函數(shù)

ReserveVirtualMemory函數(shù)的內(nèi)容: https://github.com/dotnet/coreclr/blob/release/1.1.0/src/pal/src/map/virtual.cpp#L894

CoreCLR在從系統(tǒng)申請內(nèi)存時(shí)會使用VirtualAlloc或mmap模擬的VirtualAlloc
申請后會得到一塊尚未完全提交到物理內(nèi)存的虛擬內(nèi)存(注意保護(hù)模式是PROT_NONE, 表示該塊內(nèi)存不能讀寫執(zhí)行, 內(nèi)核無需設(shè)置它的PageTable)
如果你有興趣可以看一下CoreCLR的虛擬內(nèi)存占用, 工作站GC啟動時(shí)就占了1G多, 服務(wù)器GC啟動時(shí)就占用了20G

之后CoreCLR會根據(jù)使用慢慢的把使用的部分提交到物理內(nèi)存, 流程是

GCToOSInterface::VirtualCommit => ClrVirtualAlloc => CExecutionEngine::ClrVirtualAlloc => EEVirtualAlloc => VirtualAlloc

如果是windows, VirtualAlloc是同名的windowsapi, 地址會被顯式指定且頁保護(hù)模式為可讀寫(PAGE_READWRITE)
如果是linux或者macosx, VirtualAlloc會調(diào)用VIRTUALCommitMemory, 且內(nèi)部會調(diào)用mprotect來設(shè)置該頁為可讀寫(PROT_READ|PROT_WRITE)

當(dāng)GC回收了垃圾對象, 不再需要部分內(nèi)存時(shí)會把內(nèi)存還給系統(tǒng), 例如回收小對象后的流程是

gc1 => decommit_ephemeral_segment_pages => decommit_heap_segment_pages => GCToOSInterface::VirtualDecommit

GCToOSInterface::VirtualDecommit的調(diào)用流程是

GCToOSInterface::VirtualDecommit => ClrVirtualFree => CExecutionEngine::ClrVirtualFree => EEVirtualFree => VirtualFree

如果是windows, VirtualFree是同名的windowsapi, 表示該部分虛擬內(nèi)存已經(jīng)不再使用內(nèi)核可以重置它們的PageTable
如果是linux或者macosx, VirtualFree通過mprotect模擬, 設(shè)置該頁的保護(hù)模式為PROT_NONE

VirtualFree函數(shù)的內(nèi)容: https://github.com/dotnet/coreclr/blob/release/1.1.0/src/pal/src/map/virtual.cpp#L1291

我們可以看出, CoreCLR管理系統(tǒng)內(nèi)存的方式比較底層
在windows上使用了VirtualAlloc和VirtualFree
在linux上使用了mmap和mprotect
而不是使用傳統(tǒng)的malloc和new
這樣會帶來更好的性能但同時(shí)增加了移植到其他平臺的成本

動態(tài)調(diào)試GC分配對象內(nèi)存的過程

要深入學(xué)習(xí)CoreCLR光看代碼是很難做到的, 比如這次大部分來源的gc.cpp有接近37000行的代碼, 如果直接看可以把一個(gè)像我這樣的普通人看瘋
為了很好的了解CoreCLR的工作原理這次我自己編譯了CoreCLR并在本地用lldb進(jìn)行了調(diào)試, 這里我分享一下編譯和調(diào)試的過程
這里我使用了ubuntu 16.04 LTS, 因?yàn)閘inux上部署編譯環(huán)境比windows要簡單很多

下載CORECLR:

git clone https://github.com/dotnet/coreclr.git

切換到你正在使用的版本, 請務(wù)必切換不要直接去編譯master分支

git checkout v1.1.0

參考微軟的幫助安裝好需要的包

# https://github.com/dotnet/coreclr/blob/master/Documentation/building/linux-instructions.mdecho "deb http://llvm.org/apt/trusty/ llvm-toolchain-trusty-3.6 main" | sudo tee /etc/apt/sources.list.d/llvm.listwget -O - http://llvm.org/apt/llvm-snapshot.gpg.key | sudo apt-key add -sudo apt-get update sudo apt-get install cmake llvm-3.5 clang-3.5 lldb-3.6 lldb-3.6-dev libunwind8 libunwind8-dev gettext libicu-dev liblttng-ust-dev libcurl4-openssl-dev libssl-dev uuid-dev cd coreclr ./build.sh

執(zhí)行build.sh會從微軟的網(wǎng)站下載一些東西, 如果很長時(shí)間都下載不成功你應(yīng)該考慮掛點(diǎn)什么東西
編譯過程需要幾十分鐘, 完成以后可以在coreclr/bin/Product/Linux.x64.Debug下看到編譯結(jié)果

完成以后用dotnet創(chuàng)建一個(gè)新的可執(zhí)行項(xiàng)目, 在project.json中添加runtimes節(jié)

{"runtimes": {"ubuntu.16.04-x64": {}} }

Program.cs的代碼可以隨意寫, 想測哪部分就寫哪部分的代碼,我這里寫的是多線程分配內(nèi)存然后釋放的代碼

寫完以后編譯并發(fā)布

dotnet restoredotnet publish

發(fā)布后bin/Debug/netcoreapp1.1/ubuntu16.04-x64/publish會多出最終發(fā)布的文件
把剛才CoreCLR編譯出來的coreclr/bin/Product/Linux.x64.Debug下的所有文件復(fù)制到publish目錄下, 并覆蓋原有文件
微軟官方的調(diào)試文檔可見 https://github.com/dotnet/coreclr/blob/release/1.1.0/Documentation/building/debugging-instructions.md

使用lldb啟動進(jìn)程, 這里我項(xiàng)目名稱是coreapp所以publish下的可執(zhí)行文件名稱也是coreapp

lldb-3.6 ./coreapp

啟動進(jìn)程后可以打命令來調(diào)試, 需要中斷(暫停)程序運(yùn)行可以按下ctrl+c

這張圖中的命令

b allocate_small 給函數(shù)下斷點(diǎn), 這里的allocate_small雖然全名是SVR::gc_heap::allocate_small或WKS::
gc_heap::allocate_small 但是lldb允許用短名稱下斷點(diǎn), 碰到多個(gè)符合的函數(shù)會一并截取r 運(yùn)行程序, 之前在pending中的斷點(diǎn)如果在程序運(yùn)行后可以確定內(nèi)存位置則實(shí)際的添加斷點(diǎn)bt 查看當(dāng)前的堆棧調(diào)用樹, 可以看當(dāng)前被調(diào)用的函數(shù)的來源是哪些函數(shù)


這張圖中的命令

n 步過, 遇到函數(shù)不會進(jìn)去, 如果需要步進(jìn)可以用s 另外步過匯編和步進(jìn)匯編是ni和sifr v 查看當(dāng)前堆棧幀中的變量 也就是傳入的參數(shù)和本地變量p acontext->alloc_ptr p *acontext打印全局或本地變量的值, 這個(gè)命令是調(diào)試中必用的命令, 不僅支持查看變量還支持計(jì)算表達(dá)式


這張圖中的命令

c繼續(xù)中斷進(jìn)程直到退出或下一個(gè)斷點(diǎn)br del 刪除之前設(shè)置的所有斷點(diǎn)


這張圖顯示的是線程列表中的第一個(gè)線程的分配上下文內(nèi)容, 0x168可以通過p &((Thread*)nullptr)->m_Link計(jì)算得出(就是offsetof)
這張圖中的命令

me re -s4 -fx -c12 0x00007fff5c006f00讀取0x00007fff5c006f00開始的內(nèi)存, 單位是4byte, 表現(xiàn)形式是hex, 顯示12個(gè)單位


lldb不僅能調(diào)試CoreCLR自身的代碼
還能用來調(diào)試用戶寫的程序代碼, 需要微軟的SOS插件支持
詳細(xì)可以看微軟的官方文檔 https://github.com/dotnet/coreclr/blob/release/1.1.0/Documentation/building/debugging-instructions.md

最后附上在這次分析中我常用的lldb命令
學(xué)習(xí)lldb可以查看官方的Tutorial和GDB and LLDB command examples

參考鏈接

https://github.com/dotnet/coreclr/blob/master/Documentation/botr/garbage-collection.md
https://github.com/dotnet/coreclr/blob/release/1.1.0/src/gc/gcsvr.cpp
https://github.com/dotnet/coreclr/blob/release/1.1.0/src/gc/gcwks.cpp
https://github.com/dotnet/coreclr/blob/release/1.1.0/src/gc/gcimpl.h
https://github.com/dotnet/coreclr/blob/release/1.1.0/src/gc/gcpriv.h
https://github.com/dotnet/coreclr/blob/release/1.1.0/src/gc/gc.h#L162
https://github.com/dotnet/coreclr/blob/release/1.1.0/src/vm/gchelpers.cpp#L931
https://raw.githubusercontent.com/dotnet/coreclr/release/1.1.0/src/gc/gc.cpp
https://github.com/dotnet/coreclr/blob/release/1.1.0/src/pal/src/map/virtual.cpp#L894
https://github.com/dotnet/coreclr/blob/master/Documentation/building/linux-instructions.md
https://github.com/dotnet/coreclr/blob/release/1.1.0/Documentation/building/debugging-instructions.md
https://docs.microsoft.com/en-us/dotnet/articles/core/tools/project-json
https://github.com/dotnet/coreclr/issues/8959
https://github.com/dotnet/coreclr/issues/8995
https://github.com/dotnet/coreclr/issues/9053

因?yàn)間c的代碼實(shí)在龐大并且注釋少, 這次的分析我不僅在官方的github上提問了還動用到lldb才能做到初步的理解
下一篇我將講解GC內(nèi)存回收器的內(nèi)部實(shí)現(xiàn), 可能需要的時(shí)間更長, 請耐心等待吧

原文地址:http://www.cnblogs.com/zkweb/p/6379080.html


.NET社區(qū)新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關(guān)注

總結(jié)

以上是生活随笔為你收集整理的CoreCLR源码探索(三) GC内存分配器的内部实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。