前言

  本文主要是讲解C#语言在内存中堆、栈的使用情况,使读者能更好的理解值类型、引用类型以及线程栈、托管堆。

首先感谢原文作者:Matthew Cochran 为我们带来了一篇非常好的文章,并配以大量图示,帮助我们更好的理解堆栈之间的调用,本文是在作者原文的基础上进行内容上的精简以及加入我个人在这方面的理解和注释。

最后要感谢博客园的田志良,当我搜索堆栈内部使用时,搜索到了作者的文章,吸取了大量有用的知识,而且翻译的也非常好。唯一美中不足的可能是仅仅翻译了Matthew Cochran这个系列文章的第一篇,而忽略了后面的几篇,导致内容上略有不完整,所以,我会在继续完成后续的工作,为大家答疑解惑。

下面引用作者写作此文的原因:

虽然有了.Net Framework我们不用担心内存管理和垃圾回收问题,但是我们仍然要记住内存管理和垃圾回收为了优化程序。而且,有一个内存管理如何运行的基本概念将帮助我们解释我们写的每一个程序中变量的行为。

 注:限于本人英文理解能力,以及技术经验,文中如有错误之处,还请各位不吝指出。

目录

C#堆栈对比(Part One)

C#堆栈对比(Part Two)

C#堆栈对比(Part Three)

C#堆栈对比(Part Four)

栈vs堆:不同之处

  栈负责追踪那些在我们代码中执行的内容(或者是那些被调用的内容)。而堆则负责追踪我们的对象(我们的数据,当然大多数情况下都是“数据”,稍后我会讨论这个问题)。

  注:栈类似于代码执行过程的一个容器,而堆则类似于保存数据的容器。

  把栈想象成一个一系列的盒子,一个落着一个在上面。当我们每次调用一个方法(called a Frame)时,我们通过将盒子叠加在最顶部的盒子上来观察在我们的代码中究竟发生了什么。其实,我们只能使用栈中最顶部的盒子。当我们处理完最顶部的盒子(我们执行过的方法、函数)后我们就丢弃它并且继续使用之前在顶部的盒子。堆对于栈很相似,只是堆的目的是保存信息(大多数情况下不追踪执行代码),所以在任何时刻我们的堆都能被访问。有了堆,我们将不像栈一样有那么多访问约束。堆更像是一堆在床上我们还没来得及整理的洗干净的衣服;我们能快速的得到我们想要的衣服。而栈更像是在壁橱里的一摞鞋盒,我们拿掉最顶上的鞋盒为了得到下面的盒子里的鞋。

  注:网上找了两个图片替代作者的图片,这样会更生动些。左侧为栈,右侧为堆。

  栈可自我维护,这意味着它基本只关系它自己的内存管理。当栈顶的盒子不再使用之后,随即就丢弃掉。堆,在另一方面而言,必须关心垃圾回收问题,这些问题主要是处理如何保持堆整洁(没有人喜欢乱堆脏衣服,臭气熏天~~~)。

  注:栈中的内容是每执行一次指令之后即释放掉,所以无需关注资源泄漏;堆则需要GC不定时的回收已不再使用的资源,需维护并关注性能问题。

堆栈上究竟发生了什么

在我们的堆或者栈中我们有四个主要类型:值类型、引用类型、指针类型和指令。

  1. 值类型:

  在C#中,所有的值类型均继承自System.ValueType这个抽象类

  注:System.ValueType继承自System.Object,并且重写了.ToString()等方法,以便阻止某些情况下的装箱问题。

  • bool,byte,char,decimal,double,enum,float,int,long,sbyte,short,struct,uint,ulong,ushort

  2. 引用类型:

  在C#中,如下的引用类型均继承自System.Object,当然除了Object其自身。

  • class,interface,delegate,object,string

  3. 指针类型:

  放置在我们内存管理中的第三个类型是一个引用类型,这就是我们常说的指针。我们不明确的使用指针,他们是被CLR管理的资源。指针(或者引用)是不同于引用类型的,当我们在讨论引用类型的时候,就是说我们是通过指针使用引用类型的。一个指针是指向另一个内存空间的一大块内存。一个指针占据空间就像是一个我们放置在堆或者栈中,并且它的值是一个地址或者为Null。

  4. 指令类型:

  稍后,在后面的文章中我们会分析。

如何推断类型是在堆上,还是在栈上?

  这里,我们有两条黄金定律:

  1. 引用类型总是在堆上创建,十分简单,是吧?
  2. 值类型和指针类型总是在它声明的地方创建。这有点复杂并且需要懂一点栈是如何工作的。

  栈,正如我们前面讲的,是负责追踪每一个线程中代码执行情况(或者被调用)。你可以认为它是一个线程状态并且每一个线程有它自己的状态。当我们的代码调用执行一个方法,开始执行一个已经被JIT编译过的指令,并且存活在方法表中(live on the method table),它也将参数放置在线程栈中。然后,当我们进入方法体并且带着参数执行方法时,指令将被提到栈顶部。

下面我们用一段代码来演示:

1 public int AddFive(int pValue)
2 {
3 int result;
4 result = pValue + 5;
5 return result;
6 }

  这就是发生在栈上的事情。必须记住的是我们正在观察的是已经存在于栈上的:我们执行方法并且方法参数被放置在栈中,稍后我们谈论参数细节。

此图中的AddFive方法并不存在于栈中,这里只是为了演示说明。

  下一步,命令执行到存在于我们的类型表中的AddFive()方法,如果第一次执行该方法,JIT会执行一次。

  当方法执行时,我们需要一些内存为“result”这个变量,并且这个变量将在栈上创建,如下图:

 

  方法执行完毕,返回结果。如下图:

  所有在栈上创建的内存将被清理,通过将指针指向一开始AddFive()指向的可用内存地址。

  在这个例子中,我们的“result”变量将被放置在栈中。事实上,每次当值类型带着方法体被声明时,它将被放置在栈中。

  现在,值类型有时也被放置在堆中。请记住这个规则,值类型是根据其声明的地方而决定其是在堆还是在栈上的。如果一个值类型在方法体外面声明的,但是在引用类型内部,这样它将被包裹在引用类型,并且在堆上创建。

  例子如下:

  如果我们有如下的MyInt类(类自然是一个引用类型)

public class MyInt
{
public int MyValue;
}
执行如下:
public MyInt AddFive(int pValue)
{
MyInt result = new MyInt();
result.MyValue = pValue + 5;
return result;
}

  就像刚才一样,线程开始执在线程栈上的行方法和参数,如下图:

  现在就比较有趣了。因为MyInt是一个引用类型,MyInt类型将被放置在堆中,并且被一个放置在栈中的指针所引用(指向),如下图:

  在AddFive()被执行后,我们将清理栈,如下图:

  我们只剩下一个孤独的对象在堆中(在栈中将没有任何指针指向堆中的MyInt),如下图:

  这就是GC展现实力的舞台。当我们达到一定内存瓶颈时我们需要堆中要有更多的空间,这时GC出场。GC将停止所有运行中的线程(完全停止),找出在堆中所有没有被引用的对象并且删除它们。GC将重新组织所有在堆中的对象以获得空间,调整所有在堆以及栈中的指针。就像你想象的那样,这将花费十分昂贵的性能,所以现在你就能看出当你在写高性能代码时,关注堆栈中有什么是如此的重要。

  注:1.GC回收一般发生在程序内存不够用时,否则不会发生除非手动调用。2.手动调用GC可实现强制“尝试”回收资源。3.GC中的所有资源是分“代”的,每次检测堆中的对象是否还有引用,如果有当前的“代”数加一,否则减一,GC回收“代”数最小的资源,这也就解释了为什么即使我手动调用GC.Collect()方法之后,对象还是没有马上被回收的问题。4.频繁调用GC.Collect()会导致频繁的线程中断,从而严重影响性能。

  好的,十分棒,跟我有什么关系?

  问的好。

  当我们用引用类型时,我们正在处理指针这个类型,而不是引用(实际的方法、类型)其本身;当我们用值类型时,我们就是用的类型自身。这令人费解,对吗?

  再来,下面这个例子很好的诠释了这个问题:

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!

  为什么?…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;
}

  注:实际来讲,引用类型则是指向堆中的同一个对象。

  希望这能让您对值类型和引用类型有一个更好的理解通过C#代码并且理解指针的用法和在那里使用。

在下一部分(Part Two),我们将更深入的聊一聊内存管理,尤其要是讨论方法参数。

总结

    1. 堆与栈的概念及不同点:在内存中栈主要负责处理线程中的命令,并且是以栈Stack的形式读取与执行的;堆主要是存储方法体以及数据,类似于床上散落的衣服,可供随机读取。
    2. 值类型与引用类型不同点:引用类型永远存在于托管堆上,值类型在哪取决于声明的位置。
    3. 堆和栈上的垃圾回收:栈有自我维护特性,执行完语句马上释放不会造成资源泄漏。堆则需GC回收,并且符合GC回收的规则,很多堆上的内容在程序退出前都没有被回收,很可能是无意中某处还保留着内容的引用导致,这将严重影响性能。
    4. 值类型与引用类型在改变内容时处理的方式不同:值类型执行内容拷贝,引用类型始终更改的是所引用的内容,这将导致两者行为上的不一致。

【原文地址】http://www.cnblogs.com/cuiyansong/p/4413514.html

【C#】 堆和栈的更多相关文章

  1. JVM学习(2)——技术文章里常说的堆,栈,堆栈到底是什么,从os的角度总结

    俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结如下: 堆栈是栈 JVM栈和本地方法栈划分 Java中的堆,栈和c/c++中的堆,栈 数据结构层面的堆,栈 os层面 ...

  2. java中内存分配策略及堆和栈的比较

    Java把内存分成两种,一种叫做栈内存,一种叫做堆内存 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配.当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间 ...

  3. 译文---C#堆VS栈(Part One)

    前言 本文主要是讲解C#语言在内存中堆.栈的使用情况,使读者能更好的理解值类型.引用类型以及线程栈.托管堆. 首先感谢原文作者:Matthew Cochran 为我们带来了一篇非常好的文章,并配以大量 ...

  4. 在JS中关于堆与栈的认识function abc(a){ a=100; } function abc2(arr){ arr[0]=0; }

    平常我们的印象中堆与栈就是两种数据结构,栈就是先进后出:堆就是先进先出.下面我就常见的例子做分析: main.cpp int a = 0; 全局初始化区 char *p1; 全局未初始化区 main( ...

  5. C语言堆和栈

    堆和栈的区别 一个由C/C++编译的程序占用的内存分为以下几个部分1.栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等.其操作方式类似于数据结构中的栈.2.堆区(heap ...

  6. JAVA中用堆和栈的概念来理解equals() "=="和hashcode()

    在学习java基本数据类型和复杂数据类型的时候,特别是equals()"=="和hashcode()部分时,不是很懂,也停留了很长时间,最后终于有点眉目了. 要理解equals() ...

  7. C/C++ 堆和栈的区别

    堆和栈的区别 一个由C/C++编译的程序占用的内存分为以下几个部分 1.栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等.其 操作方式类似于数据结构中的栈. 2.堆区(h ...

  8. Java堆、栈和常量池以及相关String的详细讲解(经典中的经典) (转)

    原文链接 : http://www.cnblogs.com/xiohao/p/4296088.html 一:在JAVA中,有六个不同的地方可以存储数据: 1. 寄存器(register). 这是最快的 ...

  9. JVM堆和栈的区别

    物理地址 堆的物理地址分配对对象是不连续的.因此性能慢些.在GC的时候也要考虑到不连续的分配,所以有各种算法.比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记--压缩) ...

  10. C语言堆栈入门——堆和栈的区别

    来看一个网上很流行的经典例子: main.cpp int a = 0; 全局初始化区 char *p1; 全局未初始化区 main() { int b; 栈 char s[] = "abc& ...

随机推荐

  1. java并发编程基础——线程池

    线程池 由于启动一个线程要与操作系统交互,所以系统启动一个新的线程的成本是比较高的.在这种情况下,使用线程池可以很好的提升性能,特别是程序中涉及创建大量生命周期很短暂的线程时. 与数据库连接池类似,线 ...

  2. windows程序快速启动的方式:WIN键+R

    WIN键+R是windows快速启动程序的一种方式,一般能独立运行的程序都能以这种方式启动.如notepad.calc.explorer等程序. 在命令行方式下explorer加上不同的参数,会得到不 ...

  3. YsoSerial 工具常用Payload分析之CC3(二)

    这是CC链分析的第二篇文章,我想按着common-collections的版本顺序来介绍,所以顺序为 cc1.3.5.6.7(common-collections 3.1),cc2.4(common- ...

  4. SAML 2.0 实例分析 idp向sp发送响应(4)

    当idp与user建立起联系后,idp向sp发送响应 <samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol ...

  5. C++五十一篇 -- VS2017开发人员新闻无法联网

    参考链接:https://blog.csdn.net/zz1589275782/article/details/88364983 这几天玩了下以前的电脑,本来想更新一下Visual Studio In ...

  6. DNS的原理和解析过程

    DNS的解析原理和过程: 在Internet上域名和IP是对应的,DNS解析有两种:一种是正向解析,另外一种是反向解析. 正向解析:正向解析就是将域名转换成对应的 IP地址的过程,它应用于在浏览器地址 ...

  7. springmvc学习指南 之---第25篇 Spring Bean有三种配置方式

    writed by不要张艳涛, 从tomcat转到了springmvc 现在开始有点不知道该看什么书了,看完了springmvc 学习指南之后 又查了一些书,好多都是内容相近,在找书的过程之中,发现s ...

  8. js学习笔记之正则

    () 是为了提取匹配的字符串.表达式中有几个()就有几个相应的匹配字符串.(\s*)表示连续空格的字符串.[]是定义匹配的字符范围.比如 [a-zA-Z0-9] 表示相应位置的字符要匹配英文字符和数字 ...

  9. 线性反馈移位寄存器(LFSR)

    LFSR用于产生可重复的伪随机序列PRBS,该电路有n级触发器和一些异或门组成,如下图所示. 其中,gn为反馈系数,取值只能为0或1,取为0时表明不存在该反馈之路,取为1时表明存在该反馈之路:这里的反 ...

  10. Spring Cloud分区发布实践(5)--定制ServiceInstanceListSupplier

    现在我们简单地来定制二个 ServiceInstanceListSupplier, 都是zone-preference的变种. 为了方便, 我重新调整了一下项目的结构, 把一些公用的类移动到hello ...