前言

  本文主要是讲解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),它也将参数放置在线程栈中。然后,当我们进入方法体并且带着参数执行方法时,指令将被提到栈顶部。

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

 public int AddFive(int pValue)
{
int result;
result = pValue + ;
return result;
}

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

此图中的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 + ;
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 = ;
int y = new int();
y = x;
y = ;
return x;
}

  最终的结果是3. 十分简单,不是吗?

public class MyInt
{
public int MyValue;
}
public int ReturnValue2()
{
MyInt x = new MyInt();
x.MyValue = ;
MyInt y = new MyInt();
y = x;
y.MyValue = ;
return x.MyValue;
}

  返回值是什么?答案是4!

  为什么?…x.MyValue是如何变为4的?看一看我们正在做的是什么并且是否这样做有意义:

public int ReturnValue()
{
int x = ;
int y = x;
y = ;
return x;
}

  注:值类型是传递值,而非传递引用,如下图:

  下一个例子,我们没得到“3”,因为x和y都是指向同一个堆对象的变量。

public int ReturnValue2()
{
MyInt x;
x.MyValue = ;
MyInt y;
y = x;
y.MyValue = ;
return x.MyValue;
}

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

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

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

总结

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

译文---C#堆VS栈(Part One)的更多相关文章

  1. 译文---C#堆VS栈(Part Two)

    前言 在本系列的第一篇文章<C#堆栈对比(Part One)>中,介绍了堆栈的基本功能和值类型以及引用类型在程序运行时的表现,同时也包含了指针作用的讲解. 本文为文章的第二部分,主要讲解参 ...

  2. 译文---C#堆VS栈(Part Three)

    前言 在本系列的第一篇文章<C#堆栈对比(Part Two)>中,介绍了值类型和引用类型在参数传递时的不同,本文将讨论如何应用ICloneable接口实现去修复引在堆上的用变量所带来的问题 ...

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

    前言 在本系列的第一篇文章<C#堆栈对比(Part Three)>中,介绍了值类型和引用类型在Copy上的区别以及如何实现引用类型的克隆以及使用ICloneable接口等内容. 本文为文章 ...

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

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

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

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

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

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

  7. C语言堆和栈

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

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

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

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

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

随机推荐

  1. js == 与 === 的区别

    1.对于string,number等基础类型,==和===是有区别的 1)不同类型间比较,==之比较“转化成同一类型后的值”看“值”是否相等,===如果类型不同,其结果就是不等 2)同类型比较,直接进 ...

  2. KeyedPriorityQueue

    // <copyright file="KeyedPriorityQueue.cs" company="Microsoft">Copyright ( ...

  3. jshint 一些选项(转载)

    内容来自: http://www.cnblogs.com/qianduanjingying/p/6185793.html 一些变量的作用: http://www.cnblogs.com/CloudMu ...

  4. delphi 步长不等于 1 的方法

    i:=; do begin ... INC(i,); end; 不能用for循环,只能用像上面的方法折衷一下

  5. Mac AppleScript 自动完成按键

    本人用AppleScript成功实现了打开锐捷app,并且在用户验证框输入我的用户密码,然后强制退出锐捷.(至于为什么这么做呢?用校园网的痛苦,一言难尽啊!) 学习以下内容,首先你要自行百度资料来学习 ...

  6. 弱省互测#0 t3

    Case 1 题意 要求给出下面代码的答案然后构造输入. 给一个图, n 个点 m 条边 q 次询问,输出所有点对之间最大权值最小的路径. 题解 把每一个询问的输出看成一条边,建一棵最小生成树. Ca ...

  7. JSON相关知识,转载:删除JSON中数组删除操作

    一:JSON是什么 JSONg格式:对象是一个无序的“名称/值”对的集合. 对象以括号开始,括号结束. 名称冒号分隔值. "名称/值"之间用逗号分隔 例: var people = ...

  8. ExecutorService与ThreadPoolTaskExecutor

    1.ExecutorService private static ExecutorService exec = null; public static ExecutorService getExecu ...

  9. js中的navigator对象

    Navigator 对象包含有关浏览器的信息.所有浏览器都支持该对象 在控制台中输出相关信息的代码 <script> console.log(navigator); </script ...

  10. py2exe

    1- 生成单一的exe文件: pyinstaller.py -F d:\open_txt\t.py 2- 添加必要的搜索路径: pyinstaller.py -F -p D:\python27\Lib ...