前言
虽然在.Net Framework 中我们不必考虑内在管理和垃圾回收(GC),但是为了优化应用程序性能我们始终需要了解内存管理和垃圾回收(GC)。另外,了解内存管理可以帮助我们理解在每一个程序中定义的每一个变量是怎样工作的。
简介
这篇文章我们将介绍一些方法参数传递行为在堆与栈中的影响。前几节我们介绍了堆与栈的基本工作原理,程序执行时值类型与引用类型在堆栈中的存储。另外,我们已经介绍了一些关于指针的基本知识。这一节中参数传递对堆栈的影响很重要,下面会慢慢道来。
参数,大画面
下面是当代码运行时会产生的一个详细过程。上几节已经介绍过当一个方法被调用时会产生的基本情况,让我们来看一下更加详细的内容。
当我们调用一个方法时会发生以下情形:
- 栈会分配一块内存空间给程序执行所需要的信息(我们叫它栈结构Stack Frame)。一个栈结构包含方法调用地址(指针),它以一个GOTO指令的形式存在栈里。因此,当程序执行完方法(method)时,它会知道怎么样返回进而曳继续执行代码。
- 方法的所有参数将被复制到栈里,这是我们将要更加详细介绍的部分。
- 控制被传递到JIT编译过的方法里,同时线程开始执行代码。此时,我们将有另一个方法呈现在栈结构的“回调栈”里。
代码:
- public int AddFive(int pValue)
- {
- int result;
- result = pValue + 5;
- return result;
- }
栈像下图所示:
注意:ReturnValue方法不会存在栈上,图中把ReturnValue作为此栈结构的开始只是为了解释栈原理。
像前几节介绍的,值类型和引用类型在栈里的存储是不同的。栈为任何值类型创建副本,栈也为任何引用类型的指针创建副本。
值类型传递
下面是值类型传递在栈里的内幕。
首先,当我们传递一个值类型变量时,栈会为它分配一块内存空间并把值类型变量的值存储进去。看下面的代码:
- class Class1
- {
- public void Go()
- {
- int x = 5;
- AddFive(x);
- Console.WriteLine(x.ToString());
- }
- public int AddFive(int pValue)
- {
- pValue += 5;
- return pValue;
- }
- }
当代码执行时,栈为x分配一块内存空间并存储值5
然后,AddFive()被放到栈上,同时栈分配内存空间给参数pValue并复制x的值给它。
当AddFive()执行完成,线程被传递回Go()。同时因为AddFive()执行完,它的参数pValue也实质上被移除。
所以结果是5是合理的。关键点是任何被传递的值类型参数仅是一个碳复制,因为我们希望保护原始变量的值。
有一点要记住的是,如果我们有一个非常庞大的值类型(如,庞大的struct类型)传递到栈里,当处理器循环复制它并循环占有栈空间时将会非常耗资源。栈没有无限的空间去使用,就像用水杯不断的接水早晚会溢出一样。Struct类型可以变得非常庞大,我们要小心并清醒的使用它。
下面是一个比较大的struct结构类型:
- public struct MyStruct
- {
- long a, b, c, d, e, f, g, h, i, j, k, l, m;
- }
让我们看看执行下面代码Go()方法时再到DoSomething()方法会发生的情况:
- public void Go()
- {
- MyStruct x = new MyStruct();
- DoSomething(x);
- }
- public void DoSomething(MyStruct pValue)
- {
- // 省略具体实现....
- }
这可能会非常低效。想像一下如果我们传递MyStruct几千次,它会怎么样让程序死掉。
那么,我们怎么才能回避这样的问题呢?那就是仅传递原始值类型的引用。
public void Go()
{
MyStruct x = new MyStruct();
DoSomething(ref x);
}
public struct MyStruct
{
long a, b, c, d, e, f, g, h, i, j, k, l, m;
}
public void DoSomething(ref MyStruct pValue)
{
// 省略实现....
}
{
MyStruct x = new MyStruct();
DoSomething(ref x);
}
public struct MyStruct
{
long a, b, c, d, e, f, g, h, i, j, k, l, m;
}
public void DoSomething(ref MyStruct pValue)
{
// 省略实现....
}
这样就能节省内存并提升内存使用效率
唯一需要注意的是传递引用时我们在访问原始变量x的值,任可对pValue的改变都会影响到x。
下面的代码会将x改变成"12345",因为pValue.a实际上指向原始x声明时所在的内存地址。
- public void Go()
- {
- MyStruct x = new MyStruct();
- x.a = 5;
- DoSomething(ref x);
- Console.WriteLine(x.a.ToString());
- }
- public void DoSomething(ref MyStruct pValue)
- {
- pValue.a = 12345;
- }
引用类型传递
传递引用类型跟上一节所示例中用引用的方式传递值类型相似。
如果使用引用类型(原文可能笔误,写的是值类型):
- public class MyInt
- {
- public int MyValue;
- }
然后调用Go()方法,MyInt会被放到堆里因为它是一个引用类型。
- public void Go()
- {
- MyInt x = new MyInt();
- }
如果执行下面代码中的Go():
- public void Go()
- {
- MyInt x = new MyInt();
- x.MyValue = 2;
- DoSomething(x);
- Console.WriteLine(x.MyValue.ToString());
- }
- public void DoSomething(MyInt pValue)
- {
- pValue.MyValue = 12345;
- }
会发生这种情况:
- 开始调用Go(),栈分配一块内存空间给x。
- 执行行到DoSomething(),栈分配一块内在空间给pValue。
- x的值是堆中MyInt对应在栈里的内存地址,复制x给pValue。
因此,我们用pValue改变MyInt的MyValue的值时,x最终也会获得这个改变的值"12345“。
如果我们用引用的方式传递一个引用类型变量呢?
用引用的方式传递引用类型
我们有一个类Thing, 类
Animal和Vegetables衍生于Thing:
- public class Thing
- {
- }
- public class Animal:Thing
- {
- public int Weight;
- }
- public class Vegetable:Thing
- {
- public int Length;
- }
执行下面的Go()方法:
- public void Go()
- {
- Thing x = new Animal();
- Switcharoo(ref x);
- Console.WriteLine(
- "x is Animal : "
- + (x is Animal).ToString());
- Console.WriteLine(
- "x is Vegetable : "
- + (x is Vegetable).ToString());
- }
- public void Switcharoo(ref Thing pValue)
- {
- pValue = new Vegetable();
- }
x最终变成 Vegetable。
打印结果:
- x is Animal : False
- x is Vegetable : True
让我们看看堆栈里到底发生了什么情况
- 调用Go()方法,栈分配一块内存空间给x。
- 堆分配一块内存空间给Animal。
- 开始执行Switcharoo()方法,栈分配一块内存空间给pValue并指向x。
- 栈分配一块内存空间给Vegetable。
- pValue改变了x的值使其指向Vegetable的内在地址。
如果我们不是用ref传递的,打印结果正相反。
总结
我们已经演示了参数传递是怎么在内在中处理的。在接下来的文章里,存储在栈中的引用变量会产生什么情况以及怎么解决对象复制带来的问题。
翻译: http://www.c-sharpcorner.com/UploadFile/rmcochran/csharp_memory2B01142006125918PM/csharp_memory2B.a