c#:内存中的变量会发生啥?

Posted

技术标签:

【中文标题】c#:内存中的变量会发生啥?【英文标题】:c#: What happens in a variable in memory?c#:内存中的变量会发生什么? 【发布时间】:2018-04-08 11:48:46 【问题描述】:

如果我有这个变量:

string name;

会在内存中分配一个位置吗?或者它只会在我将其初始化为特定值时分配内存?即,

string name = "Jack";

例如,考虑以下代码:

for (int i = 0; i < 20; i++) 
    Run();


private void Run() 
    int age = 20;

内存中的age 值会怎样?是否会在每次执行 Run 方法时从内存中删除?还是会在代码执行后留在内存中,在使用它的程序关闭后删除?

【问题讨论】:

相关问题Memory Allocation stack vs. heap 旁注:一个非常激进的优化器可能会注意到您的代码没有可见的效果并删除所有内容。 ....我真的需要为 VS/Roslyn 编写扩展/分析器,我想念 Eclipse 中的它 - 没有什么比看着你写的一半的东西消失了,让你对你正在使用的东西非常诚实。 啊,是的,不错的 C++ 优化编译器。您的代码中有类、函数调用和循环吗?不,你一定已经想到了 您为什么选择巴巴克的答案作为正确答案?他所说的几乎所有内容都是错误的、部分错误的或令人困惑的。 【参考方案1】:

如果你有这个代码:

void Main()

    string name;

然后,当使用编译器优化编译(在 LINQPad 中)时,您将获得以下 IL:

IL_0000: 重新

并且没有优化:

IL_0000:没有 IL_0001: 重新

没有为此声明分配内存 - 只是一个 NOP 操作作为 IL 中未优化代码的占位符。

当你的程序是这样的:

void Main()

    string name = "Jack";

那么你编译优化的代码是:

IL_0000: 重新

编译器只是忽略未使用的变量。

未优化的代码会生成这个:

IL_0000:没有 IL_0001: ldstr "杰克" IL_0006: stloc.0 // 名称 IL_0007: 恢复

显然未优化的代码更具解释性,所以从现在开始我只会展示未优化的代码,除非我明确说明。

现在让我们让代码做一些更有趣的事情。

void Main()

    string name = "Jack";
    Console.WriteLine(name);

这会产生:

IL_0000:没有 IL_0001: ldstr "杰克" IL_0006: stloc.0 // 名称 IL_0007: ldloc.0 // 名称 IL_0008:调用 System.Console.WriteLine IL_000D:没有 IL_000E: 恢复

有趣的是,如果我们把这段代码改成这样:

void Main()

    int answer = 42;
    Console.WriteLine(answer);

我们得到这个:

IL_0000:没有 IL_0001:ldc.i4.s 2A IL_0003: stloc.0 // 回答 IL_0004: ldloc.0 // 回答 IL_0005:调用 System.Console.WriteLine IL_000A:没有 IL_000B: 恢复

代码与string 示例几乎相同。

ldstr 调用正在获取对字符串字面量的引用(存储在大对象堆(不是小对象堆的普通堆)上的字符串池中)并将其推送到评估堆栈。

ldc.i4.s 正在将数字 42 的引用推送到评估堆栈。

然后,在这两种情况下,stloc.0 将评估堆栈顶部的值存储到方法的第零个本地内存位置。

然后,在这两种情况下,ldloc.0 再次从第零个本地内存位置加载值并将其放入评估堆栈。

您可能可以想象编译器在优化这段代码时会做什么。

System.Console.WriteLine 终于完成了。

现在让我们更详细地看看那个讨厌的string 代码。

我说它存储在实习生池中。让我们检查一下。

获取此代码:

void Main()

    string name = "Jack";
    Console.WriteLine(String.IsInterned(name));

它产生:

IL_0000:没有 IL_0001: ldstr "杰克" IL_0006: stloc.0 // 名称 IL_0007: ldloc.0 // 名称 IL_0008:调用 System.String.IsInterned IL_000D:调用 System.Console.WriteLine IL_0012:没有 IL_0013: 重新

它会将Jack 输出到控制台。只有在System.String.IsInterned 返回一个内部字符串时,它才能做到这一点。

拿这个程序来展示反面:

void Main()

    string name = String.Join("", new []  "Ja", "ck" );
    Console.WriteLine(String.IsInterned(name));

它将null 推送到控制台 - 这意味着字符串name 没有被保留,因此在这种情况下name 存储在堆(小对象堆)上。

让我们看看你的第二段代码:

void Main()

    for (int i = 0; i < 20; i++)
    
        Run();
    


private void Run()

    int age = 20;

如果我们查看优化的 IL,那么 Run 方法如下所示:

跑步: IL_0000: 重新

未优化的 IL 是这样的:

跑步: IL_0000:没有 IL_0001:ldc.i4.s 14 IL_0003: stloc.0 // 年龄 IL_0004: 恢复

并且,就像我之前使用int 的示例一样,它将文字值20(或十六进制的14)加载到评估堆栈中,然后立即将其存储在该方法的本地内存中,并且然后返回。因此,对于局部变量age,它会重复使用相同的内存 20 次。

【讨论】:

【参考方案2】:

在您的第一个示例(未初始化的变量)中,它不会分配任何内存,因为它不会生成任何 MSIL。这将与根本没有代码一样。 如果初始化它,内存将分配在当前方法的堆栈中。

第二种情况,每次方法调用都会在栈中分配age变量,并在每次方法调用退出时释放。

【讨论】:

实际上,在第一个示例中,只要调用该方法,它将分配堆栈上的一个变量,一个对象引用。它的大小很小,但它仍然是一个分配,所以它不是“零”。 此外,有趣的是,在foo = "mom",“mom”是编译时常量,当调用此代码时,“mom”字符串对象可能已经作为实习生存在于字符串池中,所以不会创建新对象,只会将引用写入堆栈上的变量,该变量先前已分配,因此复制的字节数很少,没有分配。如果该行类似于foo = new string(5, 'x')foo = new List&lt;int&gt;,那么这是正常情况,即 allocate+construct+assign 啊是的。并且正如另一条评论中提到的 Enigmativity 所提到的,还有 JIT 和变量将被放入寄存器的可能性.. eh @quetzalcoatl 有趣的是,我在 linqpad 上进行了测试,只有变量声明,没有初始化,它没有生成任何 MSIL。也许 linqpad 不是一个很好的参考。 对不起,我悄悄地假设我们正在讨论在代码中使用较低的变量的急切声明。喜欢string foo; ...... foo = "mom";。堆栈变量将在方法调用时立即分配,而不是在分配点。在您的情况下,使用未使用的变量,编译器只是对其进行了优化。对未使用的类成员不能这样做,但在堆栈上编译器可以很容易地省略未使用的东西。【参考方案3】:

字符串名称;

如果这是您唯一的语句,编译器可能会优化并删除它。如果没有优化,这将是对 null 的引用。

字符串名称 = "杰克";

这将在堆中创建一个内存分配来存储字符串本身。它还将在您的堆栈中生成一个指针,其中包含已分配堆内存的地址。退出方法并弹出堆栈后,堆中分配的内存将不再具有引用,并且可以标记为垃圾回收。

您的 20 次迭代将生成 20 个堆栈分配,每个堆栈分配的堆栈中的值都为 20,而堆中没有生成任何内容。退出方法后,堆栈会被弹出,数据会丢失。

【讨论】:

“未初始化(垃圾)内存位置” - 不,它最初是对 null 的引用。 “在你的堆栈中生成一个指针” - 不,这在技术上不会发生。它会生成一个“参考”,而不是一个“指针”。可能是运行时的实现使用了指针,但这不在规范中。 “将产生 20 个堆栈分配”- 并非总是如此。它可能会在堆栈上,但也可以在其他地方。 > "在桌面 CLR 上的 C# 的 Microsoft 实现中,当值是局部变量或临时值而不是 lambda 的封闭局部变量或临时值时,值类型存储在堆栈中匿名方法,并且方法体不是迭代器块,jitter选择不注册值。” “在堆中创建内存分配来存储字符串本身” - 也没有。因为这个字符串在编译时就存在,所以字符串会在实习池中创建,不会在堆上分配。【参考方案4】:

对于任何 .NET 值类型变量,例如 intbooldouble 等;内存分配在您声明它时立即发生,当您为其赋值时,该值只是在内存中更新。

另一方面,对于包括string 在内的引用类型,仅在内存中分配一个地址,该地址创建对存储当前值的实际内存位置的引用(类似于C/C++ 中的指针)。

因此,在您的示例中,age 将在 int age 运行后立即在内存中创建,然后在 age = 20 执行时其值将设置为 20

每次执行Run() 方法时都会为其分配一个新的内存位置。

【讨论】:

“对于任何 .NET 值类型变量,如 int、bool、double 等;内存分配在您声明它时立即发生” - 不完全正确 - 值类型的闭包,例如,不要这样工作。实际上是这样的:“在桌面 CLR 上的 C# 的 Microsoft 实现中,当值是局部变量或临时值而不是 lambda 或匿名方法的封闭局部变量时,值类型存储在堆栈中,并且方法体不是迭代器块,抖动选择不注册值。” @Enigmativity 别忘了拳击。

以上是关于c#:内存中的变量会发生啥?的主要内容,如果未能解决你的问题,请参考以下文章

循环中的 Lambda 变量捕获 - 这里会发生啥? [复制]

JavaScript 构造函数中的“var”变量会发生啥?

C# 中的内存泄漏

当我取消引用指针并将值分配给某个变量时,内存会发生啥?

当引用“死亡”时,引用变量会发生啥?

C#中的“对象被枚举”是啥意思?