引用类型存储在堆上,值类型存储在栈上。

17

在阅读《深入C#》时,我正在阅读标题为“引用类型存储在堆上,值类型存储在栈上”的一节。

现在我能理解的是(主要是对于引用类型):

class Program
{
    int a = 5;  // stored in heap

    public void Add(int x, int y) // x,y stored in stack
    {
        int c = x + y;  // c  stored in stack
    }
}

只是想澄清我的假设是否正确。谢谢。

编辑: 我应该使用不同的变量,因为我认为最初创建了混乱。所以我修改了代码。

编辑: 是的,正如Jon提到的 - 这是一个谬论。我应该提到的。 我很抱歉。

6个回答

18

1
它们适用于大多数现有的实现。没有什么可以阻止任何人构建一个无栈CLR。x和y在其中不会在堆栈上吗?没有什么可以阻止优化将引用类型放在堆栈上并在堆栈展开时清理它。虽然这今天还没有实现,但是它是可能的。了解堆栈和堆的处理方式很好,但只有在真正适当地选择值与引用类型之后才能进行。一方面,谈论堆栈效率的人往往低估了CLR堆的效率。 - Jon Hanna
5
@siride:我应该指出,那个部分特别指出这是一个谬论 :) - Jon Skeet
@Jon Hanna:没错,但这正是我发布的文章要点。堆栈与堆是实现细节,在考虑引用类型和值类型语义时,程序员大多数情况下应该忽略它们。 - siride
1
@Jon:这不仅是CLR实现问题,也是C#编译器实现问题。C#编译器并没有说明如何存储东西。例如,编译器可以在不更改CLR的情况下进行更改,使用一个类来存储每个方法的局部变量,而语言规范不必更改。 - Jon Skeet
2
@siride:我的意思是,我有一个神话列表,我明确表示这些神话是错误的,“引用类型存在堆上,值类型存在栈上”就是其中之一。这个问题让人觉得这本书在断言它,而实际上它是在驳斥它 :) - Jon Skeet
显示剩余4条评论

14

我可能是一个相当有用的抽象,让人们能够在幕后想象正在发生的事情。但实际上,在当前任何已发布的JIT编译器版本中都不是这样。这或许是问题的关键,实际分配位置是JIT编译器实现细节。

对于主流(x86和x64)JIT编译器来说,值类型值可能存在至少六个位置:

  • 在堆栈帧中,由局部变量声明或方法调用放置
  • 在CPU寄存器中,这是Release构建中JIT执行的非常常见的优化。还用于传递参数给方法,x86的前两个,x64的前四个。以及尽可能多地用于本地变量
  • 在FPU堆栈上,由x86 JIT用于浮点值
  • 在GC堆上,当该值是引用类型的一部分时
  • 在AppDomain的加载器堆栈中,当变量被声明为静态时
  • 在线程本地存储中,当变量具有[ThreadStatic]属性时

引用类型对象通常在GC堆上分配。但我知道一个特定的例外,从源代码中的文字字面量生成的interned字符串会在AppDomain的加载器堆上分配。这在运行时完全像对象一样运行,但它没有链接到GC堆,收集器根本看不到它。

针对您的代码片段:

  • 是的,“a”很可能存储在GC堆上
  • “x”始终在x86和x64上通过CPU寄存器传递。“y”将在x64上通过CPU寄存器传递,在x86上则在堆栈上。
  • “c”很可能根本不存在,因为JIT编译器已经将其删除,因为该代码没有效果。

1
为什么第一个参数 x 会被放在堆栈中而第二个参数 y 不总是这样呢?P.S. c 在发布模式下将被移除。 - abatishchev
2
x86核心需要两个CPU寄存器,x64核心需要四个。 "this"指针需要一个。 - Hans Passant

2
引用 Jon Skeet 在他关于 .NET 应用程序中引用类型和值类型存储的 著名博客 中的话:
变量的内存槽位可以存储在堆栈或堆上,这取决于其声明的上下文环境:
1. 每个本地变量(即在方法中声明的变量)都存储在堆栈上。这包括引用类型变量——变量本身存储在堆栈上,但请记住,引用类型变量的值仅为引用(或null),而不是对象本身。方法参数也算作本地变量,但如果它们使用ref修饰符声明,则不会获得自己的槽位,而是与调用代码中使用的变量共享一个槽位。有关更多详细信息,请参见我的参数传递文章。
2. 引用类型的实例变量始终存储在堆上。这就是对象本身所在的位置。
3. 值类型的实例变量存储在声明该值类型的变量的上下文环境中。实例的内存槽位有效地包含了实例中每个字段的槽位。这意味着(根据前面两点),在方法中声明的结构体变量将始终在堆栈上,而作为类的实例字段的结构体变量将在堆上。
4. 每个静态变量都存储在堆上,无论它是在引用类型还是值类型中声明的。无论创建多少实例,总共只有一个槽位。(尽管不需要为该槽位创建任何实例。)变量存储在哪个堆上的详细信息很复杂,但在MSDN文章中有详细解释。

0

c 留在堆栈上,因为它至少是一个值类型,而 a 在托管堆上,因为它是引用类型的字段。


2
请注意,即使变量 c 是类型为 StringBuilder 的变量,它的值在当前实现中也会在堆栈上。只是变量的值将是一个对象的引用 - 它是 对象 在堆上。我发现许多事情一旦你区分变量、它的值以及该值实际代表的内容(例如引用而不是实际对象),就会更清晰明了。 - Jon Skeet

0

以C/C++的术语来考虑。

每当你创建一个"新的"东西,或者使用malloc时,它就会放在堆上--也就是说,"对象"放在堆上,指针本身放在结构体(或函数,实际上只是另一个结构体)范围内的栈上。如果它是局部变量或引用类型(指针),它就放在栈上。

换句话说,引用类型所指向的"对象"在堆上,只是指针本身在栈上。内存泄漏发生在程序将指针从栈上弹出,但堆中的内存尚未释放可供使用--如果已经丢失了对其位置的引用,你如何知道要释放哪块内存呢?嗯,C/C++无法知道,你必须在引用被弹出栈并永远丢失之前自己做这个工作,而现代语言则通过它们那些花哨的垃圾收集堆来处理这个问题。相比由GC来回收,显式清理你分配的堆内存仍然是更好的选择,这样更"省性能"(从CPU资源的角度来看)。


0
引用类型的存储位置(变量、字段、数组元素等)保存对堆上对象的引用;原始值类型的存储位置将其值保存在自身内部;结构类型的存储位置将它们的所有字段(每个字段都可以是引用或值类型)保存在自身内部。如果一个类实例包含两个不同的非空字符串、一个点和一个整数,则点的X和Y坐标以及独立的整数和对两个字符串的引用将保存在一个堆对象中。每个字符串将保存在不同的堆对象中。关于类与结构的存储位置的关键点是,除了类实体持有对自身的引用的情况外,在类或结构中的每个非空引用类型字段都将保存对某个其他对象的引用,该对象将位于堆上。

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