我很想了解C#对象引用在运行时(.NET CLR中)是如何在内存中表示的。一些相关问题:
一个对象引用占用多少内存?它在类范围和方法范围下有所不同吗?它所在的位置是栈还是堆,取决于这个范围吗?
对象引用实际维护什么数据?它只是指向所引用对象的内存地址,还是还有其他内容?定义在类或方法的作用域中是否有所不同?
当引用传递到方法中时,与上述相同的问题,即关于对引用的引用,这时候答案1和2会发生什么变化?
我很想了解C#对象引用在运行时(.NET CLR中)是如何在内存中表示的。一些相关问题:
一个对象引用占用多少内存?它在类范围和方法范围下有所不同吗?它所在的位置是栈还是堆,取决于这个范围吗?
对象引用实际维护什么数据?它只是指向所引用对象的内存地址,还是还有其他内容?定义在类或方法的作用域中是否有所不同?
当引用传递到方法中时,与上述相同的问题,即关于对引用的引用,这时候答案1和2会发生什么变化?
.NET堆栈和堆的工作原理 本文详细介绍了堆和栈的工作原理。
C#和许多其他使用堆的面向对象编程语言在一般引用语境下使用句柄而不是指针。指针类比适用于一些通用概念,但是这种概念模型在像这样的问题上会崩溃。请参阅Eric Lippert在此主题上的优秀帖子Handles are Not Addresses
说一个句柄的大小与指针相同是不合适的(尽管它可能巧合相同)。句柄是对象的别名,它们不必是对对象的正式地址。
在这种情况下,CLR碰巧使用句柄的实际地址:从上面的链接中:
…CLR实际上将托管对象引用实现为拥有垃圾回收器拥有的对象的地址,但这是一项实现细节。
因此,在32位体系结构上,句柄可能是4字节,而在64位体系结构上,它可能是8字节,但这并不是“确定的”,也不是直接与指针相关的。值得注意的是,根据编译器实现和使用的地址范围,某些类型的指针的大小可能会有所不同。
在此背景下,您可能可以通过指针类比来模拟这个问题,但重要的是要意识到句柄不需要是地址。如果CLR想要在未来更改此内容,则CLR的消费者不应该知道任何更好的方法。
这个微妙点的最后一个推动:
这是C#指针:
int* myVariable;
这是一个 C# 句柄:
object myVariable;
它们不是同一个东西。
你可以对指针执行诸如数学运算之类的操作,但不能对句柄(Handle)执行这样的操作。如果你的句柄像指针一样实现,并且你将其用作指针,则在某些方面上误用句柄可能会在以后给你带来麻烦。
int*
就是 C# 指针类型的一个例子。 - Eric Lippert如果您理解C/C++指针,那么这个答案是最容易理解的。指针简单来说就是某些数据的内存地址。
对象引用应该与指针的大小相同,在32位CPU上通常为4字节,在64位CPU上为8字节。它的大小不会因其定义位置而改变,但它所在的位置依赖于定义位置。如果它是类的字段,则将驻留在对象所在的堆中。如果它是静态字段,则位于堆的特殊部分,不受垃圾回收的影响。如果它是局部变量,则位于堆栈上。
对象引用只是一个指针,可以将其视为包含内存中对象地址的int或long类型。无论它在哪里定义,都是相同的。
这是通过指向指针的指针实现的。数据相同-只是一个内存地址。但是,给定内存地址上没有对象。相反,有另一个内存地址,即对对象的原始引用。这就是允许修改引用参数的原因。通常,参数在其方法完成后消失。由于对象的引用不是参数,因此对该引用的更改将保留。引用到引用将消失,但引用不会。这就是传递引用参数的目的。
有一件事需要知道,值类型被存储在原地(没有内存地址,相反它们直接存储在内存地址的位置 - 见#1)。当它们传递给一个方法时,会创建一个副本,并在该方法中使用该副本。当它们通过引用传递时,会传递一个内存地址,该地址可定位于内存中的值类型,从而允许更改。
编辑:正如dlev指出的那样,这些答案并不是硬性规定,因为没有规定必须如此实现。.NET可以自由地以任何方式实现这些问题。但这是最可能实现它的方式,因为这就是英特尔CPU在内部工作的方式,因此使用任何其他方法可能会低效。
希望我没有把你搞糊涂,如果需要解释,请随时问。