本文属摘录,所有权归原作者所有!
内容比较详细,直观,容易理解!
在.NET中或许我们不用担心内存管理以及垃圾回收器(Garbage Collection GC)的问题,但是我们还是应该了解这些东东以便在必要的时候优化我们程序的性能。而且,如果对内存管理如何工作有所了解,那将有助于解释我们每个程序里的每个变量的运行规律。这篇文章主要内容是解释堆(Heap)和栈(Stack),各种变量以及这些变量到底是如何工作的。
.Net Framework 在执行代码时,有两个用来存储对象的地方,也就是堆和栈,用于帮助执行我们的代码。它们驻留在机器内存中,包含了所有我们需要实现的信息。
Stack VS Heap
栈多多少少用来负责跟踪你的代码里正在执行什么,或者说代码里的什么东东被called。而堆则或多或少用来跟踪我们的对象,或者说数据,大多数情况下都是数据啦——后头再详解。
把栈想象成堆砌起来由上到下的盒子。每次我们调用一个方法,就新加一个盒子到栈顶,我们用这种方法跟踪我们的程序在执行些什么。我们能用的,永远只是最顶上的那个盒子。当我们把最顶上这个盒子用掉了的时候,也就是方法执行完毕并返回的时候,我们就恶狠狠的把它扔掉!然后接着处理下一个盒子里的东东。而堆其实也是类似的东东,除了它的目的是用来保存信息(绝大多数情况下不是用来跟踪程序执行),因此堆里面的任何东西都不受限制的随便访问。堆就像是床上我们没空收拾的洗好的衣服一般,我们能很快的随便拿任何一件起来。而栈则跟壁橱里一堆装鞋子的盒子似的,我们得一个一个的从顶上取下来才能拿到下一双鞋子。
上图虽然并不是真正的内存中堆栈的样子,不过有助于我们理解它们的区别。
栈是“自我维护”的,意思是基本上是管理自个儿的(而不是别的地方的)内存。当顶部盒子不在使用,就扔之(就不是自家的雪了)!而堆呢,不太一样的是,必须得跟GC打交道——这东西用来保证堆是clean(没有过多垃圾内存)的。(木有人喜欢地上摆一堆脏衣服吧!臭死了!)。
What goes on the Stack and Heap?
当代码执行时,堆栈里头主要放置四种类型的东东:值类型,引用类型,指针(Pointers),以及指令(Instructions)。
值类型:
c#中,值类型继承自System.ValueType:
bool, byte, char, decimal, double, enum, float, int, long, sbyte, short, struct, uint, ulong, ushort
引用类型:
而引用类型则有:
class, interface, delegate, object, string
指针:
内存管理模型中的第三种东东是对一个类型的引用。这个引用通常就是指指针。我们不能直接使用指针,它们被CLR所管理。指针不同于引用类型。当我们说某某是引用类型时,实际上就意味着我们要通过指针去访问这个类型的值。一个指针占用内存中的一块空间,只想内存中另外一块空间。指针跟任何别的放在堆栈中的东西一样,是要占用物理空间的。它的值要么是null,要么就是内存地址。
指令:
在后续文章中再解释,稍安勿躁……
How is it decided what goes where? (Huh?)
okok,再啰嗦两句我们就可以正式摆弄我们的堆栈了。
这里有两条黄金规则:
- 引用类型总是保存在堆里头——够清楚了吧
- 值类型以及指针,总是保存在其被声明(Declared)的地方。稍微复杂一丁点儿,因为需要理解什么是“其被声明的地方”
栈,就像我们刚才提到的,负责跟踪单个线程(thread)运行到哪儿了。你不妨把其想象成thread的状态机,每个线程都有自个儿的栈。当我们的代码调某个方法时,线程开始执行JIT编译过并且保存在方法表(method table)中的指令集,同时,它把方法参数压入线程栈中。然后开始执行代码并访问方法里需要的、同时已经存在于线程栈顶部的变量。举个例子吧:
public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}
让我们看看栈里头都发生了什么,记住我们看到的只是栈顶的东西,下头早有无数别的东东在里面了哦!
当我们开始执行这个方法时,方法的参数被压栈(稍后我们讨论参数传递)。
Notice: 方法并不存在stack里头,图例只是为了演示概念
下一步,控制(线程执行这个方法)被交给AddFive方法在方法表中的指令集,如果这是第一次使用这个方法,JIT编译将被执行。
在方法的执行过程中,我们需要内存来保存 "result”,因此栈顶为其分配空间。
方法结束了,result被返回。
这时栈顶指针将会移到最初AddFive方法开始的内存地址,这样所有刚才分配的内存空间都被清理掉了,然后接着执行AddFive更下面的函数(图中为显示)。
在这个例子里,result变量被压栈。事实上,任何时候值变量在方法内部被声明,都会被压栈。
不过,值变量有时候也会存储在堆里头。看看黄金规则二,值类型总是保存在被声明的地方。那么,如果值类型在方法(method)外部声明,同时本身又存在于引用类型内部时,那么就会被保存在堆里头。
在来一个例子:
如果我们有MyInt Class(引用类型):
public class MyInt
{
public int MyValue;
}
另外一个方法正在执行中:
public MyInt AddFive(int pValue)
{
MyInt result = new MyInt();
result.MyValue = pValue + 5;
return result;
}
跟前头一样,线程开始执行方法,参数被压栈:
这个时候开始好玩了:
因为MyInt是引用类型,因此保存在堆里头,通过栈里的指针去引用它。
AddFive执行完毕以后,我们开始清理栈顶:
这样我们就把MyInt对象当作孤儿留在了堆里头,因为栈中没有任何指针在引用它了!
这时候就是GC大显身手的时候啦!一旦我们程序达到某个特定的内存阀值(threshold)而我们需要更多堆空间时,GC就会开动。GC完全停止所有运行中的线程,找到所有堆里没有被引用的对象,然后像秋风扫落叶般的删除它们。GC重新组织所有滞留在内存中活动的对象,并调整堆栈中对其引用的地址。可以看到,从性能角度来说,这可真是很费时间。所以现在你是不是觉得注意一下堆栈的处理会有助于你写出高性能的代码呢?
OK,好了好了,很棒很棒,不过,这东西到底会如何来折磨俺们呢?
好问题。
当我们使用引用类型时,我们实际上是使用指向该对象的指针,而不是对象本身。当我们使用值类型时,我们使用的就是值本身。清楚还是不清楚?
最好还是来个例子吧。
如果我们执行以下方法:
public int ReturnValue()
{
int x = new int();
x = 3;
int y = new int();
y = x;
y = 4;
return x;
}
我们得到3,够简单吧。
如果这样呢:
public class MyInt
{
public int MyValue;
}
public int ReturnValue2()
{
MyInt x = new MyInt();
x.MyValue = 3;
MyInt y = new MyInt();
y = x;
y.MyValue = 4;
return x.MyValue;
}
那么我们将得到4。
为什么?
在第一个例子里所有事都按照预定计划行事:
public int ReturnValue()
{
int x = 3;
int y = x;
y = 4;
return x;
}
在后面的例子里,我们得不到3是因为x和y都指向堆里的同一个对象。
public int ReturnValue2()
{
MyInt x;
x.MyValue = 3;
MyInt y;
y = x;
y.MyValue = 4;
return x.MyValue;
}
希望本文能帮助您更好的理解值类型和引用类型基本的区别,以及指针是什么,什么时候会用到指针。在后面的系列中,我们进一步的阐述内存管理,特别的会多聊聊方法参数的问题。