如何在C#中递归计算复杂对象的大小(不使用GC)
如何在不触发GC的前提下计算复杂对象的内存占用
你需要计算包含值类型、字符串、List、Dictionary等成员的复杂对象总内存大小,且全程不能触发GC。先明确你的对象结构(已整理为规范C#代码):
class Student { public string Name; public int Age; } class Section { public int SectionNumber; public double SectionAverage; public string SectionName; public List<Student> Students; public List<string> Subjects; public Dictionary<string, dynamic> StudentMapping; }
下面给出具体实现思路和代码:
核心思路
要在无GC的前提下计算内存,需递归遍历所有成员的实际占用,同时避免任何托管内存分配:
- 基础值类型直接用
sizeof()计算,无GC触发 - 引用类型需计算对象本身大小+其指向的实例内存
- 内置集合(List、Dictionary)需读取私有内部字段来计算,避免调用会分配内存的公共方法
- 提前缓存反射字段信息,避免重复反射分配对象
关键实现细节
1. 固定大小的基础单元
- 64位系统下,对象头占24字节,数组头占24字节,指针占8字节(32位系统需对应改为12、12、4字节)
- 字符串内存=对象头+字符数组大小(每个char占2字节)
- 值类型直接用
Marshal.SizeOf()获取大小
2. 自定义类型与集合的递归计算
- 自定义类:计算对象头+所有字段大小(含对齐),再递归计算每个引用类型字段的实例内存
- List
:读取私有字段 _items(内部数组),计算List自身大小+数组及元素的总内存 - Dictionary<K,V>:读取私有字段
_entries和_buckets数组,计算Dictionary自身大小+两个数组及内部元素的总内存
完整实现代码
using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; public static class MemorySizeCalculator { private static readonly Dictionary<Type, FieldInfo[]> _fieldCache = new(); // 64位系统参数,32位需修改对应值 private const int ObjectHeaderSize = 24; private const int ArrayHeaderSize = 24; private const int PointerSize = 8; static MemorySizeCalculator() { // 提前缓存需要计算的类型字段 CacheTypeFields(typeof(Student)); CacheTypeFields(typeof(Section)); CacheTypeFields(typeof(List<>)); CacheTypeFields(typeof(Dictionary<,>)); } private static void CacheTypeFields(Type type) { if (!_fieldCache.ContainsKey(type)) { _fieldCache[type] = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); } } public static long CalculateSize(object obj) { if (obj == null) return 0; Type type = obj.GetType(); // 处理值类型 if (type.IsValueType) { return Marshal.SizeOf(obj); } // 处理字符串 if (type == typeof(string)) { var str = (string)obj; return ObjectHeaderSize + str.Length * 2; } // 处理数组 if (type.IsArray) { var arr = (Array)obj; int elementCount = arr.Length; Type elementType = type.GetElementType(); long elementSize = elementType.IsValueType ? Marshal.SizeOf(elementType) : PointerSize; long total = ArrayHeaderSize + elementCount * elementSize; // 递归计算引用类型元素的内存 if (!elementType.IsValueType) { for (int i = 0; i < elementCount; i++) { total += CalculateSize(arr.GetValue(i)); } } return total; } // 处理List<T> if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) { var itemsField = _fieldCache[type].First(f => f.Name == "_items"); var items = (Array)itemsField.GetValue(obj); // List自身大小:对象头+3个指针/值类型字段(_items, _size, _version) long total = ObjectHeaderSize + PointerSize * 3; total += CalculateSize(items); return total; } // 处理Dictionary<K,V> if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { var entriesField = _fieldCache[type].First(f => f.Name == "_entries"); var bucketsField = _fieldCache[type].First(f => f.Name == "_buckets"); var entries = (Array)entriesField.GetValue(obj); var buckets = (Array)bucketsField.GetValue(obj); // Dictionary自身大小:对象头+8个指针/值类型字段(简化计算,可按实际字段数调整) long total = ObjectHeaderSize + PointerSize * 8; total += CalculateSize(entries); total += CalculateSize(buckets); return total; } // 处理自定义引用类型 long totalSize = ObjectHeaderSize; long currentOffset = 0; foreach (var field in _fieldCache[type]) { Type fieldType = field.FieldType; long fieldSize; if (fieldType.IsValueType) { fieldSize = Marshal.SizeOf(fieldType); } else { fieldSize = PointerSize; // 递归计算引用指向的对象内存 totalSize += CalculateSize(field.GetValue(obj)); } // 字段对齐(按8字节对齐) currentOffset = (currentOffset + 7) / 8 * 8; currentOffset += fieldSize; } // 对象总大小按8字节对齐 totalSize = (totalSize + 7) / 8 * 8; return totalSize; } }
注意事项
- 代码默认适配64位.NET环境,32位系统需修改
ObjectHeaderSize、ArrayHeaderSize、PointerSize的对应值 dynamic类型实际运行时为object,会按引用类型递归计算其实际实例大小- 字符串如果是 interned 状态,实际内存会被共享,但代码计算的是当前对象引用的实例大小,符合常规业务需求
- 字段缓存仅需初始化一次,避免重复反射产生的内存分配
内容的提问来源于stack exchange,提问作者Curry
相关产品推荐
相关产品推荐

