You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

C# Dictionary变量的内存存储方式及内存分配位置解析

C# Dictionary内存存储全解析

让我把这个问题拆成两部分讲——先搞清楚变量本身的存储,再深入Dictionary实例的内部结构,这样就一目了然了:

1. 变量的存储:栈上的引用,堆上的实例

首先要明确:Dictionary<TKey, TValue>引用类型,完全遵循C#引用类型的内存规则:

  • 如果是方法里的局部变量:比如你写var dict = new Dictionary<int, string>();dict这个变量本身只是一个4/8字节的引用(取决于32/64位系统),它会存在当前方法的栈帧里。而new关键字创建的Dictionary实例,包括所有的键值对、内部数组这些核心数据,全在托管堆上。
  • 如果是类的成员变量:这个引用会作为类实例的一部分,跟着类对象一起存在堆上,不会单独在栈里。

2. Dictionary实例的内部内存结构

Dictionary底层是哈希表实现的,堆上的实例主要包含这几个关键部分:

  • 哈希桶数组:这是一个动态扩容的数组,每个元素对应一个哈希“桶”。在旧版.NET里,每个桶是一个链表的头节点(用来处理哈希冲突);从.NET Core 2.0开始,优化成了开放寻址的结构数组,直接把键值对存在数组里,用空位标记处理冲突,减少了链表节点的内存开销。
  • 键值对条目(Entry):每个键值对都会被包装成一个结构体,包含:
    • 预计算的哈希码:避免每次查找都重新计算键的哈希值,提升性能
    • 键(TKey):如果是值类型,直接存实际数据;如果是引用类型,存指向堆上实例的引用
    • 值(TValue):和键的存储规则一致,值类型存数据,引用类型存引用
    • 状态标记:标记这个位置是空闲、已使用还是已删除,用于哈希冲突的处理
  • 辅助字段:比如当前元素数量、版本号(用来防止枚举时修改集合的线程安全检查)、自定义哈希比较器等,这些也都存在堆上的实例中。

3. 关键内存细节

  • 初始化与扩容:刚创建的Dictionary默认哈希桶数组长度是0,第一次添加元素时会扩容到4;当元素数量达到负载因子 * 数组长度(默认负载因子0.72)时,会把数组长度翻倍,重新哈希所有元素到新数组里——这个过程会产生临时内存占用,之后旧数组会被GC回收。
  • 值类型vs引用类型的差异:如果你的键或值是值类型(比如intDateTime),它们的实际数据会直接嵌入到堆上的Entry结构体里;如果是引用类型(比如string、自定义类),Entry里只存一个指向对应堆实例的引用,实际数据还是在堆的其他位置。

举个直观的代码例子:

public void UseDictionary()
{
    // dict是栈上的引用,指向堆上的Dictionary实例
    var dict = new Dictionary<int, string>();
    // int是值类型,直接存在堆的Entry里;string是引用类型,Entry存指向"World"字符串实例的引用
    dict.Add(42, "World");
}

额外提一句:栈的空间非常有限(一般只有几MB),像Dictionary这种可能存储大量数据的结构,必然要放在堆上——栈根本装不下这么多内容,这也是引用类型设计的核心初衷之一。

内容的提问来源于stack exchange,提问作者AmirAsadi

火山引擎 最新活动