托管對象本質-第一部分-布局




托管對象本質-第一部分-布局

原文地址:https://devblogs.microsoft.com/premier-developer/managed-object-internals-part-1-layout/
原文作者:Sergey
譯文作者:傑哥很忙

目錄

托管對象本質1-布局
托管對象本質2-對象頭布局和鎖成本
托管對象本質3-托管數組結構
托管對象本質4-字段布局

托管對象的布局非常簡單:托管對象包含實例數據、指向元數據的指針(也稱為方法表指針)和內部信息包(也稱為對象頭)。

譯者補充: 方法表指針在某些文章也被稱之為類型句柄,英文是TypeHandle。

當我第一次看到對象的布局時,我產生了一些疑問:

  1. 為什么對象的布局如此怪異?
  2. 為什么托管引用指向對象的中間,而對象頭的偏移量為負?
  3. 對象頭中存儲了哪些信息?

譯者補充:作者對於布局怪異實際指的就是對象頭的偏移量為負數。由於托管對象以引用地址的偏移量記為0,對象頭大小為4或8字節(取決於是32位還是64位,實際64位對象頭也僅使用4字節,前面4個字節填充0)。因此由於對象頭在對象指針之前,因此它的偏移量位-4或-8。

當我開始思考布局並做了一個快速研究時,我只有幾個選擇:

  1. JVM 從一開始就對托管對象使用了類似的布局。
    今天聽起來有點瘋狂,但請記住,由於Java早就有了一個特性(又名數組協方差),C#在借鑒Java語言時也引用了這一有史以來最糟糕的特性。與這個決定相比,重用一些關於對象結構的想法聽起來並不合理。

    譯者補充:數組協方差可能存在無法保證類型安全,從而產生一個運行時異常。詳情可以看與C#數組的協方差和逆差

  2. 對象頭的大小可以增大,而在 CLR 中沒有橫切更改。
    對象頭包含 CLR 使用的一些輔助信息,CLR 可能需要比指針大小字段更多的信息。事實上,移動電話中使用的 .Net Compact Framework 對於大小對象具有不同的頭(有關詳細信息,請參閱 WP7:CLR 托管對象開銷)。桌面 CLR 從未使用過此功能,但這並不意味着將來不可能實現此功能。

    譯者補充:這里說的CLR沒有根據切面大小改變對象頭,指的是桌面CLR,因為移動設備的CLR會根據對象大小改變對象頭的布局。

  3. 緩存行和其他性能相關特征。

Chris Brumme – CLR 架構師之一,在他的發表的Value Types的評論中提到,緩存友好性正是托管對象布局的原因。從理論上講,由於緩存行大小(64 字節),訪問彼此較近的字段的效率可能更高。這意味着根據字段在對象中的位置不同,訪問間接引用字段會有不同的性能差異。我花了一些時間試圖證明對於現代處理器該理論依據仍然是成立的,但無法獲得任何能夠顯示存在的差異基准測試數據。

花了一些時間試圖驗證我的理論后,我聯系了Vance Morrison問這個問題,並得到了以下的答案:目前的設計沒有特別考慮。

因此,對於"為什么托管對象的布局如此怪異?"的一個簡單回答是由於歷史原因造成的。老實說,我可以看到在負索引移動對象頭的邏輯,以強調此數據塊是 CLR 的實現細節,它的大小可以隨時間而變化,並且不應由用戶檢查。

譯者補充:原作者說的是由於歷史原因造成的也沒有毛病,因為非托管代碼就包含了對象頭和對象引用地址,因此托管代碼延續了這一風格。

現在是時候審視布局的更多細節了。再次之前,我們思考一下,CLR可以與托管對象實例關聯哪些額外信息?以下是一些想法:

  • GC可以用來標記可從應用程序根訪問對象的特殊標志。
  • 一種特殊的標志用於通知GC某個對象已固定,在垃圾收集期間不應移動。
  • 托管對象的哈希代碼(當未重寫GetHashCode方法時)。
  • 鎖語句使用的關鍵節和其他信息:獲取鎖的線程等。

除了實例狀態之外,CLR還存儲了許多與類型相關的信息,如方法表、接口映射、實例大小等等,但這與我們當前的討論無關。

IsMarked 標記

托管對象頭可用於多種不同的用途。你可能認為垃圾收集器(GC)使用對象頭中的一個位來標記該對象是由根引用的,並且應該保持活動狀態。這是一種常見的誤解,只有少量的名著提及。

比如Jeffrey Richter寫的《CLR via C#》, 《Pro .NET Performance》作者是Sasha Goldstein,當然還有一些其他人.

CLR 作者決定不使用對象頭,而是使用一個巧妙的技巧:方法表指針的最低位用於存儲在垃圾回收期間存儲對象可訪問且不應被回收的標志。

下面是來自Coreclr的一個mark標記的實現,在文件gc.cpp的8974行:

#define marked(i) header(i) -> IsMmarked();
#define set_marked(i) header(i)->SetMarked()
#define clear_marked(i) header(i)->ClearMarked()
 
// class CObjectHeader
BOOL IsMarked() const
{
    return !!(((size_t)RawGetMethodTable()) & GC_MARKED);
}
void ClearMarked()
{
    RawSetMethodTable(GetMethodTable());
}
void SetMarked()
{
    RawSetMethodTable((MethodTable*)(((size_t)RawGetMethodTable()) | GC_MARKED));
}
MethodTable* GetMethodTable() const
{
    return((MethodTable*)(((size_t)RawGetMethodTable()) & (~(GC_MARKED))));
}

20200123114440.png

由於gc.cpp文件太大導致GitHub不分析它。 這意味着我不能將超鏈接添加到特定代碼行。

CLR 堆中的托管指針以4個或8個字節地址長度進行對齊,取決於32位還是64位平台。這意味着每個指針的 2 或 3 位始終為 0,可用於其他目的。JVM 也使用同樣的技巧,稱為"壓縮 Oops",該功能允許 JVM 具有 32 GB 堆大小,並且仍使用 4 個字節作為托管指針。

譯者補充:當我們在對象上標注StructLayout以控制對象的分布甚至偏移值。若對象沒有填滿4字節或8字節時,CLR會進行自動填充。
“這意味着每個指針的 2 或 3 位始終為 0,可用於其他目的。”對於這句話的解釋,個人理解如下:由於64位指針最多支持2^64^內存,即16TiB的內存大小,而對於windows系統則有軟件上的內存大小限制,windows7旗艦版支持192GB的內存,而windows server 2008 R2支持2TiB內存大小,Windows Server 2012提高到4TiB的最大內存限制。因此可以如作者所說,windows 64位操作系統預留了2到3位指針用於其他目的,因此最大內存支持4TiB。

從技術上講,即使在 32 位平台上,也有 2 位可用於標志。基於 object.h 文件的注釋,我們可以認為確實如此,並且方法表指針的第二個最低位用於固定(以標記在垃圾回收的壓縮階段不應移動對象)。不幸的是,並不能判斷該說法是否正確,因為來自 gc.cpp(行 3850-3859)的 SetPinned/IsPinned 方法基於對象頭中的保留位實現,並且我無法在 coreclr 代碼版本庫中找到實際設置方法表指針位的任何代碼。

下次我們會討論所得實現以及鎖的性能消耗大小。

相關文獻

  1. 數組協方差
  2. CPU高速緩存行對齊(cache line)
  3. 類型實例的創建位置、托管對象在托管堆上的結構
  4. .net托管環境下struct實例字段的內存布局(Layout)和大小(Size)
  5. 托管堆上對象的大小(Size)和Layout
  6. .NET對象的內存布局
  7. .NET Framework Internals: How the CLR Creates Runtime Objects
  8. What limits Windows 7 x64 machines to <=192GB RAM?

20191127212134.png
微信掃一掃二維碼關注訂閱號傑哥技術分享
出處:https://www.cnblogs.com/Jack-Blog/p/12230616.html
作者:傑哥很忙
本文使用「CC BY 4.0」創作共享協議。歡迎轉載,請在明顯位置給出出處及鏈接。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM