託管對象本質-第一部分-佈局



託管對象本質-第一部分-佈局

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

目錄

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

託管對象的佈局很是簡單:託管對象包含實例數據、指向元數據的指針(也稱爲方法表指針)和內部信息包(也稱爲對象頭)。windows

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

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

  1. 爲何對象的佈局如此怪異?
  2. 爲何託管引用指向對象的中間,而對象頭的偏移量爲負?
  3. 對象頭中存儲了哪些信息?

譯者補充:做者對於佈局怪異實際指的就是對象頭的偏移量爲負數。因爲託管對象以引用地址的偏移量記爲0,對象頭大小爲4或8字節(取決因而32位仍是64位,實際64位對象頭也僅使用4字節,前面4個字節填充0)。所以因爲對象頭在對象指針以前,所以它的偏移量位-4或-8。安全

當我開始思考佈局並作了一個快速研究時,我只有幾個選擇:微信

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

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

  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
微信掃一掃二維碼關注訂閱號傑哥技術分享
出處:http://www.javashuo.com/article/p-bmfgoxgy-mm.html 做者:傑哥很忙 本文使用「CC BY 4.0」創做共享協議。歡迎轉載,請在明顯位置給出出處及連接。

相關文章
相關標籤/搜索