首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> 程式設計綜合問答

C# 獲取對象佔用內存大小的正確姿勢:sizeof、Marshal.SizeOf 與序列化的深度對比

     編輯:程式設計綜合問答

核心結論

C# 無法直接精確獲取託管對象在托管堆上的真實內存佔用大小。 這是由 CLR 內存模型(GC、對象頭、引用、對齊、壓縮等)決定的。

各方法適用場景與局限性

1. sizeof 操作符

int size = sizeof(int);        // 4 - 編譯時常量,僅限基本值類型
int size = sizeof(MyStruct);   // 錯誤:結構體無預定義大小,需 unsafe 上下文
  • 適用:基本類型 (`bool, byte, short, int, long, float, double, decimal, IntPtr, UIntPtr`)、enum、不包含引用類型字段的 `struct` (在 `unsafe` 上下文)。
  • 不適用:類 (`class`)、含引用字段的結構體、數組、字符串、任何託管對象實例。

2. Marshal.SizeOf<T>()


[StructLayout(LayoutKind.Sequential)]
struct PureValueStruct { public int A; public double B; }

[StructLayout(LayoutKind.Sequential)]
class MixedClass { public int A; public string B; } // 含引用類型

// 正確
int s1 = Marshal.SizeOf(); // 非托管大小:16 字節 (含對齊)

// 運行時拋異常:ArgumentException "無法封送類型..."
int s2 = Marshal.SizeOf(); 
  • 返回:類型的非托管表示大小(用於 P/Invoke、COM 互操作、二進制序列化)。
  • 要求:類型必須有確定的佈局 (`StructLayout.Sequential` 或 `Explicit`),且不可包含引用類型字段(字符串、數組、類、接口、委托、Object)。
  • 注意:非托管大小 ≠ 託管堆大小。託管對象額外包含:對象頭 (8/16 字節)、方法表指針、同步塊索引,且字段排序可能不同。

3. 二進制序列化測量 (BinaryFormatter)


[Serializable]
class Student { public string Name; }

long GetObjectSize(object obj) {
    using (var ms = new MemoryStream()) {
        new BinaryFormatter().Serialize(ms, obj);
        return ms.Length;
    }
}
// 結果:new Student() 約 139 字節,包含大量元數據(程序集名、類型名、字段名、版本等)
  • 本質:序列化流大小,包含完整類型信息,便於反序列化重建對象圖。
  • 誤區:非內存佔用大小。同一對象序列化多次大小固定,但內存中可能因 GC 壓縮、引用共享而不同。

真實內存佔用估算方法

方法一:GC.GetTotalMemory (統計大量對象平均值)


long before = GC.GetTotalMemory(true);
var list = new List();
for (int i = 0; i < 100000; i++) list.Add(new Student { Name = "Test" + i });
long after = GC.GetTotalMemory(true);
long avgSize = (after - before) / 100000; // 統計平均佔用
  • 優點:反映真實堆佔用(含對象頭、對齊、字符串駐留等)。
  • 缺點:受 GC 時機、大對象堆 (LOH)、字符串驻留池影響,需大樣本統計。

方法二:Profiler 工具 (Visual Studio Diagnostic Tools, dotMemory, PerfView)

專業內存分析器可展示:

  • 對象實例大小 (Shallow Size / Retained Size)
  • 對象佈局、引用鏈、GC 根引用
  • 代碼熱路徑內存分配

方法三: unsafe 手動計算 (僅限無引用結構體)


unsafe {
    int size = sizeof(MyStruct); // 編譯期常量
    // 或運行時
    MyStruct s = new MyStruct();
    int runtimeSize = sizeof(MyStruct); // 相同
}

什麼決定了類的實例大小?

因素說明
非靜態實例字段值類型直接內聯;引用類型存儲指針 (4/8 字節)
對象頭SyncBlockIndex (4/8B) + MethodTablePtr (4/8B) = 8B (x86) / 16B (x64)
對齊填充實例大小向 8 字節 (x64) 對齊
靜態字段/屬性/方法不佔用實例內存,屬於類型/方法表/靜態存儲區
字符串/數組/其他引用對象單獨在堆上分配,實例僅存引用指針

常見誤區辨析

「屬性會增加對象大小」 → 錯。自動屬性編譯為私有字段,字段才佔內存。只讀屬性、表達式主體屬性無後備字段則不佔用。

「Marshal.SizeOf 返回對象內存大小」 → 錯。返回非托管佈局大小,用於互操作編組。

「序列化大小 = 內存大小」 → 錯。序列化包含類型元數據,且不計對象頭、對齊、引用共享。

總結建議

  1. **不需要精確值**:了解大致結構即可,關注「引用類型字段存指針、值類型內聯、對象頭 16B (x64)」。
  2. **性能調優/內存洩漏排查**:直接用 Profiler (VS Diagnostic Tools / dotMemory / PerfView),不要造輪子。
  3. **P/Invoke/二進制協議**:用 `Marshal.SizeOf` 配合 `StructLayout`,確保非托管佈局正確。
  4. **學術研究/教學演示**:`GC.GetTotalMemory` 大樣本統計法最接近真實。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved