【问题标题】:how are C# object references represented in memory / at runtime (in the CLR)?C# 对象引用如何在内存中/在运行时(在 CLR 中)表示?
【发布时间】:2012-02-29 00:20:58
【问题描述】:

我很想知道 C# 对象引用是如何在运行时(在 .NET CLR 中)在内存中表示的。想到的一些问题是:

  1. 对象引用占用多少内存?在类的范围与方法的范围中定义时是否有所不同?它所在的位置是否会根据这个范围(堆栈与堆)而有所不同?

  2. 在对象引用中维护的实际数据是什么?它只是一个指向它所指对象的内存地址还是更多?这是否会根据它是否定义在类或方法的范围内而有所不同?

  3. 与上述相同的问题,但这次是在讨论对引用的引用时,就像在将对象引用通过引用传递给方法时一样。 1 和 2 的答案有何变化?

【问题讨论】:

  • 请注意,这些问题都是实现细节(可能会发生变化),实际上并不是关于 C#,而是关于 .NET CLR。​​
  • Chopperdave,好有趣的问题,但我想问一下您是否在问您的意思 - 对象引用主要是一个指针,这只是一个“数字”,具体取决于您的代码的系统架构正在运行。如果您询问 .Net 堆分配的工作原理,那完全是另一回事。
  • 只是想补充一下,这里没有侮辱的意思,我并不是要暗示您不知道您的意思-问题是,在.Net中这是一个模棱两可的问题,并且它将帮助 Stack Overflow 上的未来用户准确了解我们正在谈论的上下文。
  • @RussC 未采取任何措施。在这种情况下,我对我不知道自己在问什么的想法持开放态度:)
  • @dlev 谢谢。我会编辑标题+标签来表示。

标签: c# memory clr object-reference


【解决方案1】:

.NET Heaps and Stacks 这是对堆栈和堆如何工作的彻底处理。

C# 和许多其他使用堆的 OOP 语言在一般引用中使用 Handles not Pointers 作为这种上下文中的引用(C# 也能够使用指针!)指针类比适用于一些一般概念,但是这个概念模型会因为这样的问题而崩溃。请参阅 Eric Lippert 关于此主题的出色帖子 Handles are Not Addresses

说句柄是指针的大小是不恰当的。(尽管可能巧合的是)句柄是对象的别名,并不要求它们是正式地址到一个对象。

在这种情况下,CLR 恰好使用真实地址作为句柄:来自上面的链接:

...CLR 实际上确实将托管对象引用实现为 垃圾收集器拥有的对象的地址,但这是一个 实施细节。

所以是的,句柄在 32 位架构上可能是 4 个字节,而在 64 字节架构上可能是 8 个字节,但这不是“确定的”,它不是直接因为指针。值得注意的是,根据编译器的实现和某些类型的指针使用的地址范围大小可能不同

有了所有这些上下文,您可能可以通过指针类比对其进行建模,但重要的是要意识到句柄不需要是地址。如果将来 CLR 愿意,CLR 可以选择更改这一点,而 CLR 的消费者不应该知道更多。

这个微妙点的最终驱动:

这是一个 C# 指针:

int* myVariable;

这是一个 C# 句柄:

object myVariable;

它们不一样。

您可以在指针上做数学运算,而您不应该用 Handles 做这些事情。如果您的句柄碰巧像指针一样实现,并且您像使用指针一样使用它,那么您在某些方面滥用了句柄,这可能会给您以后带来麻烦。

【讨论】:

  • 这不是 C# 指针,因为创建指向托管引用类型的指针是非法的。 int* 是 C# 指针类型的示例。
【解决方案2】:

如果您了解 C/C++ 指针,这个答案最容易理解。指针只是一些数据的内存地址。

  1. 对象引用应该是指针的大小,在 32 位 CPU 上通常为 4 个字节,在 64 位 CPU 上为 8 个字节。无论在哪里定义都是一样的。它生活在哪里取决于它被定义的地方。如果它是一个类的字段,它将驻留在它所属的对象的堆中。如果是静态字段,则位于堆中不受垃圾回收影响的特殊部分。如果它是一个局部变量,它就存在于堆栈中。

  2. 对象引用只是一个指针,可以将其可视化为包含对象在内存中的地址的 int 或 long。无论在哪里定义都是一样的。

  3. 这被实现为指向指针的指针。数据是一样的——只是一个内存地址。但是,在给定的内存地址处没有对象。取而代之的是另一个内存地址,它是对对象的原始引用。这就是允许修改参考参数的原因。通常,参数在其方法完成时消失。由于对对象的引用不是参数,因此对该引用的更改将保留。对引用的引用将消失,但引用不会消失。这就是传递参考参数的目的。

您应该知道的一件事是,值类型存储在适当的位置(没有内存地址,而是直接存储在内存地址所在的位置 - 参见 #1)。当它们被传递给一个方法时,会制作一个副本并在该方法中使用该副本。当它们通过引用传递时,会传递一个内存地址,该地址定位内存中的值类型,允许对其进行更改。

编辑:正如 dlev 指出的那样,这些答案并不是硬性规定,因为没有规定必须如此。 .NET 可以随意实现这些问题。这是最有可能实现它的方法,因为这就是 Intel CPU 在内部的工作方式,因此使用任何其他方法都可能效率低下。

希望我没有让您太困惑,但如果您需要澄清,请随时询问。

【讨论】:

  • 值类型并不总是存储在堆栈中。
  • “值类型存储在堆栈中”:这不是真的。值类型可以存储在堆栈中,但并非总是如此。例如。引用类型中的值类型字段存储在堆上,在它所属的对象中。无论如何+1,因为你的答案大多是正确的......
  • @ThomasLevesque:删除了对堆栈上值类型的引用。谢谢!
  • +1 以获得全面的答案,但正如其他人所说,值类型是规则的例外。不过,我的直觉是,虽然这是 OP 所要求的,但可能不是他的意思!
  • 非常清晰和全面。谢谢你。幸运的是,很久以前我参加了一个关于 C++ 数据类型的随机课程,并且指针被普遍引用(不是双关语)。
猜你喜欢
  • 2011-05-29
  • 1970-01-01
  • 2023-03-31
  • 1970-01-01
  • 1970-01-01
  • 2016-04-07
  • 2010-09-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多