c#:内存中的变量会发生什么?
c#: What happens in a variable in memory?
如果我有这个变量:
string name;
会在内存中分配一个位置吗?或者它只会在我将其初始化为特定值时获得分配的内存?即,
string name = "Jack";
例如,考虑以下代码:
for (int i = 0; i < 20; i++) {
Run();
}
private void Run() {
int age = 20;
}
内存中的age
值会发生什么变化?每次执行 运行 方法时,它会从内存中删除吗?还是会在代码执行后留在内存中,在使用它的程序关闭后移除?
string name;
如果这是你唯一的声明,编译器可能会优化并删除它。如果不进行优化,这将是对 null 的引用。
string name = "Jack";
这将在堆中创建一个内存分配来存储字符串本身。它还会在您的堆栈中生成一个指针,其中包含分配的堆内存的地址。退出方法并弹出堆栈后,堆中分配的内存将不再有引用,可以标记为垃圾回收。
您的 20 次迭代将生成 20 个堆栈分配,每个分配在堆栈中的值为 20,而在堆中不生成任何内容。退出该方法后,将弹出堆栈并丢失数据。
对于任何 .NET 值类型变量,如 int
、bool
、double
等;内存分配会在您声明它并为其赋值时立即发生,该值只会在内存中更新。
另一方面,对于包括string
在内的引用类型,只在内存中分配一个地址,该地址创建对存储当前值的实际内存位置的引用(类似于[=27中的指针) =]++).
因此,在您的示例中,一旦 int age
为 运行,就会在内存中创建 age
,然后它的值将设置为 20
age = 20
被执行。
每次执行Run()
方法时都会分配一个新的内存位置。
在您的第一个示例(未初始化的变量)中,它不会分配任何内存,因为它不会生成任何 MSIL。这将与根本没有代码相同。
如果你初始化它,内存将在当前方法的堆栈中分配。
第二种情况,age变量会在每次方法调用时分配到栈中,并在每次方法调用退出时释放。
如果您有此代码:
void Main()
{
string name;
}
然后,当使用编译器优化进行编译(在 LINQPad 中)时,您将获得以下 IL:
IL_0000: ret
并且没有优化:
IL_0000: nop
IL_0001: ret
没有为此声明分配内存 - 只是一个 NOP 操作作为未优化代码在 IL 中的占位符。
当你的程序是这样的:
void Main()
{
string name = "Jack";
}
那么你编译器优化后的代码是:
IL_0000: ret
编译器会简单地忽略未使用的变量。
未经优化的代码生成此:
IL_0000: nop
IL_0001: ldstr "Jack"
IL_0006: stloc.0 // name
IL_0007: ret
显然未优化的代码更易于解释,所以从现在开始我将只显示未优化的代码,除非我明确说明。
现在让我们让代码做一些更有趣的事情。
void Main()
{
string name = "Jack";
Console.WriteLine(name);
}
这会产生:
IL_0000: nop
IL_0001: ldstr "Jack"
IL_0006: stloc.0 // name
IL_0007: ldloc.0 // name
IL_0008: call System.Console.WriteLine
IL_000D: nop
IL_000E: ret
有趣的是,如果我们将此代码更改为:
void Main()
{
int answer = 42;
Console.WriteLine(answer);
}
我们得到这个:
IL_0000: nop
IL_0001: ldc.i4.s 2A
IL_0003: stloc.0 // answer
IL_0004: ldloc.0 // answer
IL_0005: call System.Console.WriteLine
IL_000A: nop
IL_000B: ret
代码与 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: nop
IL_0001: ldstr "Jack"
IL_0006: stloc.0 // name
IL_0007: ldloc.0 // name
IL_0008: call System.String.IsInterned
IL_000D: call System.Console.WriteLine
IL_0012: nop
IL_0013: ret
并输出 Jack
到控制台。它只能在 System.String.IsInterned
returns 一个 interned 字符串时这样做。
拿这个程序来展示相反的:
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
方法如下所示:
Run:
IL_0000: ret
未优化的IL是这样的:
Run:
IL_0000: nop
IL_0001: ldc.i4.s 14
IL_0003: stloc.0 // age
IL_0004: ret
并且,就像我之前使用 int
的示例一样,它将文字值 20
(或十六进制的 14
)加载到计算堆栈中,然后立即将其存储在该方法的本地内存,然后是 returns。因此它对局部变量 age
.
重复使用相同的内存 20 次
如果我有这个变量:
string name;
会在内存中分配一个位置吗?或者它只会在我将其初始化为特定值时获得分配的内存?即,
string name = "Jack";
例如,考虑以下代码:
for (int i = 0; i < 20; i++) {
Run();
}
private void Run() {
int age = 20;
}
内存中的age
值会发生什么变化?每次执行 运行 方法时,它会从内存中删除吗?还是会在代码执行后留在内存中,在使用它的程序关闭后移除?
string name;
如果这是你唯一的声明,编译器可能会优化并删除它。如果不进行优化,这将是对 null 的引用。
string name = "Jack";
这将在堆中创建一个内存分配来存储字符串本身。它还会在您的堆栈中生成一个指针,其中包含分配的堆内存的地址。退出方法并弹出堆栈后,堆中分配的内存将不再有引用,可以标记为垃圾回收。
您的 20 次迭代将生成 20 个堆栈分配,每个分配在堆栈中的值为 20,而在堆中不生成任何内容。退出该方法后,将弹出堆栈并丢失数据。
对于任何 .NET 值类型变量,如 int
、bool
、double
等;内存分配会在您声明它并为其赋值时立即发生,该值只会在内存中更新。
另一方面,对于包括string
在内的引用类型,只在内存中分配一个地址,该地址创建对存储当前值的实际内存位置的引用(类似于[=27中的指针) =]++).
因此,在您的示例中,一旦 int age
为 运行,就会在内存中创建 age
,然后它的值将设置为 20
age = 20
被执行。
每次执行Run()
方法时都会分配一个新的内存位置。
在您的第一个示例(未初始化的变量)中,它不会分配任何内存,因为它不会生成任何 MSIL。这将与根本没有代码相同。 如果你初始化它,内存将在当前方法的堆栈中分配。
第二种情况,age变量会在每次方法调用时分配到栈中,并在每次方法调用退出时释放。
如果您有此代码:
void Main()
{
string name;
}
然后,当使用编译器优化进行编译(在 LINQPad 中)时,您将获得以下 IL:
IL_0000: ret
并且没有优化:
IL_0000: nop IL_0001: ret
没有为此声明分配内存 - 只是一个 NOP 操作作为未优化代码在 IL 中的占位符。
当你的程序是这样的:
void Main()
{
string name = "Jack";
}
那么你编译器优化后的代码是:
IL_0000: ret
编译器会简单地忽略未使用的变量。
未经优化的代码生成此:
IL_0000: nop IL_0001: ldstr "Jack" IL_0006: stloc.0 // name IL_0007: ret
显然未优化的代码更易于解释,所以从现在开始我将只显示未优化的代码,除非我明确说明。
现在让我们让代码做一些更有趣的事情。
void Main()
{
string name = "Jack";
Console.WriteLine(name);
}
这会产生:
IL_0000: nop IL_0001: ldstr "Jack" IL_0006: stloc.0 // name IL_0007: ldloc.0 // name IL_0008: call System.Console.WriteLine IL_000D: nop IL_000E: ret
有趣的是,如果我们将此代码更改为:
void Main()
{
int answer = 42;
Console.WriteLine(answer);
}
我们得到这个:
IL_0000: nop IL_0001: ldc.i4.s 2A IL_0003: stloc.0 // answer IL_0004: ldloc.0 // answer IL_0005: call System.Console.WriteLine IL_000A: nop IL_000B: ret
代码与 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: nop IL_0001: ldstr "Jack" IL_0006: stloc.0 // name IL_0007: ldloc.0 // name IL_0008: call System.String.IsInterned IL_000D: call System.Console.WriteLine IL_0012: nop IL_0013: ret
并输出 Jack
到控制台。它只能在 System.String.IsInterned
returns 一个 interned 字符串时这样做。
拿这个程序来展示相反的:
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
方法如下所示:
Run: IL_0000: ret
未优化的IL是这样的:
Run: IL_0000: nop IL_0001: ldc.i4.s 14 IL_0003: stloc.0 // age IL_0004: ret
并且,就像我之前使用 int
的示例一样,它将文字值 20
(或十六进制的 14
)加载到计算堆栈中,然后立即将其存储在该方法的本地内存,然后是 returns。因此它对局部变量 age
.