所有的静态成员都存储在哪里?

46

我正在尝试学习C#如何管理内存。我卡在静态元素上,我阅读了许多关于这个主题的博客和文章,但是我找不到一个令人满意的答案。

让我们定义一个代码块来帮助找到答案。

class myClass
{
    static string myStr = "String Data";
    static int myInt = 12;
}
在你们分享答案之前,让我分享一下我对这个主题的了解。请随意赞成或反对,并帮助我找到正确的答案。
静态只是寿命问题。
静态引用类型(myStr)将在堆上保存,寿命为期。
静态值类型(myInt)将在堆栈上保存,寿命为期。
困惑之处在于我在互联网上发现的某些关于此主题的答案。
困惑1:
当您的程序启动时,它会将所有相关程序集加载到AppDomain中。加载程序集时,将调用所有静态构造函数,包括静态字段。它们将活在那里,唯一卸载它们的方法是卸载AppDomain。
在上面的几行中,明确提到了所有静态元素存储在AppDomain上。那么为什么每个人都说'Static'元素存储在堆/栈上?
困惑2:
无论静态变量是在引用类型还是值类型中声明,每个静态变量都存储在堆上。
如果每个静态变量都存储在堆上。那么为什么有些人说值类型静态变量存储在堆栈上?
请帮助连接我的思路,以了解C#中静态变量的内存管理。非常感谢您宝贵的时间 :)

15
有些人会误解“堆栈”这些概念,但其实它们并没有什么用处。在C#中,如果你只关心一个对象的生命周期或声明的作用域,那么更重要的概念是它们。垃圾回收意味着95%的情况下,你只需要关心一个对象是否还存活着,而被static字段引用的对象将与类一起加载并保持存活状态。(至于何时实例化,那是一个更复杂的话题。)当然,这并不是一个答案。 - Jeroen Mostert
1
因为他们误解了。他们知道值类型的本地变量存储在堆栈上(实际上所有本地变量都存储在堆栈上。混淆在于对于引用类型,变量是引用而不是对象)。静态变量就像Type对象的成员一样,而Type不是值类型。(当然,与Java不同,C#实际上没有Type<myClass>类型,以使每个Type是不同的类型并具有不同的成员更清晰) - Random832
9
并不是所有的局部变量都存储在堆栈上。闭包中的局部变量不在堆栈上。迭代器块中的局部变量不在堆栈上。异步方法中的局部变量不在堆栈上。寄存器中的局部变量不在堆栈上。省略的局部变量也不在堆栈上。不要再认为局部变量存在于堆栈中,这是不正确的。局部变量被称为“局部”是因为它们的名称具有本地作用域,而不是因为它们存储在堆栈上。 - Eric Lippert
@AliAsad:假设你有一个包含一千个整数的数组。这些整数是在堆栈上还是堆上?它们是装箱的还是未装箱的? - Eric Lippert
2
@AliAsad: 你认为为什么会发生装箱?反正什么是装箱呢?你的问题表明你不理解什么是装箱;你认为“boxed”和“存储在堆上”是相同的,但它们根本不是相同的东西。了解为什么人们对C#有完全错误的认识对我很有帮助;你为什么认为“boxing”意味着“存储在堆上”呢? - Eric Lippert
显示剩余6条评论
3个回答

64
首先请注意,所有这些都是实现细节。运行时保证的唯一一点是:
  • 当您请求一个静态字段时,它就在那里
  • 一个静态构造函数在您使用该类型之前的某个时间点执行
那就是全部了。其他所有内容都是实现细节 - 规范不关心堆栈、堆或其他任何内容。这取决于运行时的实现,一个有效的运行时可以将所有内容放在堆栈上(如果它愿意的话),也可以放在堆上。还有寄存器。
现在让我们看看你已经掌握的一些误解:
  • 静态只是寿命 - 是的。它并没有说明它在何时或何处被存储 - 只是在您请求时可用。符合规范的运行时可以自由地使用任何内存,甚至可以从未将字段加载到内存中(例如,保持在图像中,图像已经在内存中)
  • 静态将永久存储在堆中 - 很可能是这样。但它不是规范的一部分,符合规范的运行时可以在任何它想要的地方存储它,或者根本不存储它,只要正确的保证保持不变。此外,不要忘记,“永久”意味着“至少在AppDomain的生命周期内”;当域被卸载时可能会释放它,也可能不会。
  • 静态值类型将在堆栈上永久存储 - 很可能不是这样。同样,这是一个实现细节,但是栈具有完全不同的语义,对于静态值来说没有什么意义。下一点将给你更多的理由:
  • 当程序集被加载时,所有静态构造函数都会被调用,包括静态字段。 - 不是这样的。没有这个要求,也没有这样的保证。如果你依赖于这一点,你的程序将会出错(我以前见过很多次)。这只是一个实现细节,但在当前 MSCLR 实现中,静态变量 tend to be allocated in a heap of their own,并且在需要它们所定义的类型之前的一段时间内进行分配。如果你在静态构造函数中抛出异常,你可以很容易地看到这一点 - 它会导致一个 TypeLoadException,在第一次引用该类型的方法中(不用说,这可能会使调试静态成员变得棘手)。
  • 引用类型存储在堆上,值类型存储在栈上。 - 不是这样的。这混淆了机制和语义。两者之间唯一的区别是它们的语义,其他一切都取决于实现。如果运行时可以在堆栈上保留引用类型的引用语义,那么这是完全有效的。即使在当前的 MSCLR 运行时中,值类型也经常被存储在堆中 - 比如当它们被装箱或成为引用类型的成员时。
  • 有些人可能会感到困惑。有些人不理解合同和实际实现之间的区别。有些人根本不知道他们在说什么。我希望有一种简单的方法可以知道哪个是哪个,但是没有。如果你有疑问,你可以去看 C#/CLR 规范,但那只告诉你合同,而不是实际情况。

    托管内存的整个意义在于你不需要关心这些实现细节。当然,像任何抽象一样,它会泄漏出来 - 知道事情真正是如何的,从 CPU 微指令、内存缓存等方面,穿过所有的层和抽象,是有意义的。但这并不是你要"依赖"的 - 实现随时都可以改变,而且过去已经多次改变了。


    我同意你的观点,这都是可以随着时间变化的具体实现细节。所以我得出结论,AppDomain有它自己的静态堆。这个堆负责管理值类型和引用类型的静态成员。 - Ali Asad
    是的,基本上是这样。它仍然在垃圾回收期间被考虑(它可能具有对其他堆上对象的引用),但实际上并没有被回收。此外,请注意字符串是特殊的 - 默认情况下,所有文字字符串都是内部化的,因此如果您的静态变量具有字符串值,则该值本身很可能在另一个堆中(通常是大对象堆)。 - Luaan
    @AliAsad 至于合同方面的事情,不同应用程序域中的静态内容必须被隔离。这并不要求对象在不同的堆中,但肯定会更加方便 :) - Luaan
    @JanDvorak 是的,完全正确。过早优化会带来负面影响。但这不仅仅是关于微观优化 - 它更多地涉及到理解微小的事情,比如“我应该按行还是按列排列我的二维数据?”将决策隐藏在抽象之后,以保持代码的清晰和可读性,但也要理解抽象背后的重要性。 - Luaan
    3
    @Luaan。我对这个主题进行了一点研究。我发现有8种类型的堆。其中两种是高频堆和低频堆。所有静态成员都在高频堆中。我们用来存储引用类型对象的堆实际上是一个低频堆。想要了解更多信息,请访问此处此处 - Ali Asad

    8
    每当一个进程在RAM中被加载时,我们可以说内存大致分为三个区域(在该进程内):堆栈、堆和静态区域(在.NET中,实际上是堆内部的特殊区域,仅称为高频堆)。静态区域持有“静态”成员变量和方法。什么是静态?没有必要创建类实例的那些方法和变量被定义为静态。
    了解更多信息请点击这里

    请注意,这只是CodeProject上的一篇文章,没有参考资料,也没有从规范中引用任何内容,也没有.NET设计或实现人员的工作。这是一个初学者的介绍,甚至显然是错误的(静态方法存储在堆上?)。 - Luaan

    2
    一个类的实例被创建,所有静态成员都被初始化。
    静态类的成员通常存储在堆上,值类型的成员通常存储在栈上。
    这并不一定是绝对的,你可以阅读this博客获取更多信息。
    这篇博客是由C#语言设计者Eric Lippert撰写的。
    该博客表明,与正常知识相反,值类型不一定在栈上,引用类型不一定在堆上,但它们通常在那里。
    只是规范没有具体说明。

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接