【问题标题】:How would the memory look like for this object?这个对象的内存是什么样子的?
【发布时间】:2020-02-11 10:16:28
【问题描述】:

我想知道这个类(它的对象)的内存布局是怎样的:

class MyClass
{
    string myString;

    int myInt;

    public MyClass(string str, int i)
    {
        myString = str;
        myInt = i;
    }
}

MyClass obj = new MyClass("hello", 42);

谁能想象一下?

更新:

根据 Olivier Rogier 的回答以及 ckuri 和 Jon Skeet 的 cmets,我试图提出一个高级图表,深受 ckuri 提到的 devblog article 的影响。

据我了解:

  1. obj(8 字节引用)指向包括元数据的对象(实际上不是指向它的开头,但为了简单起见我们忽略它)。

  2. 在此存储myIntmyString 引用值(这是对真实字符串值的引用)

我不想深入到最后的细节,但我仍然好奇的是:

  1. 如果要访问obj.myString,是否需要两次“查找”,例如首先查找obj,然后跟随它并查找myString 或者是否有类似全局地址表的东西,其中直接存储了obj.myString 的地址?

  2. obj 的参考值存储在哪里?它是program 对象块的一部分吗,例如myStringobj 对象块的一部分? (假设obj 是在program 实例中创建的)

【问题讨论】:

  • 我很困惑你在问什么
  • 第二个问题编号为 (1) 中的“应访问”是什么意思?你能举一个“访问”的例子吗?我也不明白你所说的“全局地址表”是什么意思。
  • 另外,了解您提出这些问题的目的也会有所帮助;绝大多数 C# 开发人员永远不必担心这些东西。您想在这里解决一些更深层次的问题吗?如果是这样,请说出问题所在,我们可以帮助您直接解决问题。
  • 另外,您的图表没有正确显示字符串对象的结构,这比您在此处显示的要复杂得多;你在乎吗?

标签: c# memory


【解决方案1】:

这里存储了 myInt 和 myString 引用值(这是对真实字符串值的引用)

让我们确保您不会在这里走坏路。

首先,与源代码相比,我不清楚为什么要对图中的整数和字符串进行重新排序。它是实现定义的字符串和整数如何打包,以什么顺序,以及是否有任何填充字节。如果您关心这些细节,请提出更明确的问题。

其次,不清楚您所说的“真正的字符串值”是什么意思。字符串是引用类型。 字符串的真正价值是引用。字符串的 contents 的值在引用的位置。

如果要访问 obj.myString,是否需要两次“查找”,例如首先查找 obj,然后跟随它并查找 myString

我假设“查找”是指取消引用

例如,如果我们有:

var obj = whatever;
char c = obj.myString[1];

那么是的,我们有两个取消引用。 . 取消引用 obj 以获取 myString,这是一个引用。 [1] 取消引用 myString 以获取 char

obj的引用值存放在哪里?

obj 是一个变量。变量是一个存储位置。该存储位置可以位于多个位置:

  • 如果obj 是短暂的,或者更好的是,短暂的,那么它可以注册或放入短期池。 (通常称为堆栈,但在我看来,从语义上考虑短期池是一个更好的习惯,即寿命不超过激活的存储。堆栈是实现细节。)

  • 如果不知道 obj 是短期的,那么它会进入长期池,也称为托管堆。

【讨论】:

  • 什么是“临时”对象?和Ephemeral generations and segments有关系吗?
  • @LucaCremonesi:在这种情况下,我所说的“短暂”是指:考虑像int x = Foo(); int y = x + Bar(); Blah(y); 这样的方法主体片段,其中xy 没有在主体的其他任何地方使用。编译器将生成代码以生成用于激活方法的堆栈帧;它必须在框架顶部为当地人保留多少个插槽?看起来对于两个 int 来说已经足够了,但编译器可能会推断该程序与 Blah(Foo() + Bar()) 相同并生成零个保留槽。
  • @LucaCremonesi:在这种情况下,变量xy 可以变成“短暂的”。它们的存储仅在使用变量时存在,因为存储只是在需要时推送到评估堆栈(在 IL 中)。然后,抖动会将其转换为堆栈推送或寄存器分配,只要它认为合适,堆栈帧就会变得稍微小一些。这是一个小的优化,但它加起来。但是,它会使程序更难调试并且生命周期比您预期的要短,因此编译器并不总是采用这种优化。
  • @LucaCremonesi:不幸的是,C# 编译器团队选择“ephemeral”来指代短期变量中生命周期最短的变量,同时 GC 团队选择它来表示“最短的-lived 的长寿命变量”。它们之间没有任何关系,只是在这两种情况下,我们指的是存储的生命周期比您预期的要短。
  • @EricLippert 感谢您的回复,并给了我正确的术语,例如取消引用 :-) 与 Luca 的讨论也很有趣 :-)
【解决方案2】:

类或结构的每个实例都有一个用于数据的“个人内存空间”,但所有对象的方法共享一次。

首先,您需要 x32 上的 4 个字节或 x64 上的 8 个字节来存储对对象内存地址的引用(引用是忘记管理它的隐藏指针)。

接下来,对象在这里有两个数据成员:

  • 一个占 4 个字节的整数。
  • 这里的一个字符串需要 5 个字符:5x2 字节 = 10 字节。

所以对于数据,对象在 x32 上占用 18 个字节,在 x64 系统上占用 22 个字节。

由于字符串对象包含一个整数作为长度,大小略大于:x32 上为 22,x64 上为 26。

由于字符串是引用,我们需要再添加 4 或 8 个字节 => 26 或 34 个字节。

由于 string 在类声明中还有其他一些静态和实例字段,例如第一个 char,所以它需要的比这多一点。

Is string actually an array of chars or does it just have an indexer?

除此之外,代码段中的内存还有方法代码的指令。此代码适用于所有实例。

除此之外,还有类表和虚表来描述类型、方法签名和多态规则。

如果对象在某个方法中实例化,则它使用堆内存。

如果对象在声明中被实例化为类成员,我不知道 .NET 是如何工作的,但它可能分配在进程的数据段中。

而记忆就像一列火车,货车就是字节。

这是内存的伪图。

这不是很真实的现实,但它可能有助于理解:

Does accessing a variable in C# class reads the entire class from memory?

C# Heap(ing) Vs Stack(ing) In .NET

字节是内存的基本单位,每次存储一个值,介于 0 和 255(无符号)或 -128 和 +127(有符号)之间。

Learn the basics about C# data types' variables

Shifting Behavior for Signed Integers

A Tutorial on Data Representation


今天(2021.01.28)看到这个草图我意识到它可能具有误导性,这就是为什么我写“这不是非常真实的现实,但它可能有助于理解”,因为在实际上,当进程启动并存储时,方法的实现代码是从二进制文件 EXE 和 DLL 加载并存储在 CODE SEGMENT 中,因为所有数据、静态(文字)和动态(实例)都在 DATA SEGMENT 中(如果自 x32 和保护模式以来事情没有改变)。方法的非虚拟表以及方法的虚拟表不存储在每个对象实例的数据段中。我不记得细节,但这些表是用于代码的。此外,对象的每个实例的数据都是其定义及其祖先的投影,在一个地方,一个完整的实例。

Memory segmentation

x86 memory segmentation

【讨论】:

  • 这并不完全正确。对象也有一个object header and a method table。字符串是引用类型,因此会有对实际字符串对象的引用。字符串也有长度信息。实际的 MyClass 对象只是对象头、对方法表的引用、整数和对字符串对象的引用。
  • 字符串没有对 char 数组的引用——文本数据直接在字符串对象中。
  • @JonSkeet, ckuri, Olivier Rogier:尝试了高级可视化,添加到我原来的问题中。你们可以检查一下,也许还看看这两个新问题吗? :-)
  • @OlivierRogier 谢谢!由于所有涉及的工作和大量内容,被接受为答案,视觉效果的额外奖励;-)
猜你喜欢
  • 2022-08-11
  • 2012-09-04
  • 1970-01-01
  • 2020-10-03
  • 2018-05-25
  • 2011-02-04
  • 1970-01-01
  • 2012-01-05
  • 1970-01-01
相关资源
最近更新 更多