C#初学者经常被问的几道辨析题,值类型与引用类型,装箱与拆箱,堆栈,这几个概念组合之间区别,看完此篇应该可以解惑。

  俗话说,用思想编程的是文艺程序猿,用经验编程的是普通程序猿,用复制粘贴编程的是2B程序猿,开个玩笑^_^。

  相信有过C#面试经历的人,对下面这句话一定不陌生:

  值类型直接存储其值,引用类型存储对值的引用,值类型存在堆栈上,引用类型存储在托管堆上,值类型转为引用类型叫做装箱,引用类型转为值类型叫拆箱。

  但仅仅背过这句话是不够的。

  C#程序员不必手工管理内存,但要编写高效的代码,就仍需理解后台发生的事情。

  在学校的时候老师们最常说的一句话是:概念不清。最简单的例子,我熟记了所有的微积分公式,遇到题就套公式,但一样会有套不上解不出的,因为我根本不清楚公式是怎么推导出来的,基本的原理没弄清楚。

  (有人死了,是为了让我们好好的活着;有人死了,也不让人好好活:牛顿和莱布尼茨=。=)。

  有点扯远了。下面大家来跟我一起探讨下C#堆栈与托管堆的工作方式,深入到内存中来了解C#的以上几个基本概念。

一,stack与heap在不同领域的概念

  C/C++中:

  Stack叫做栈区,由编译器自动分配释放,存放函数的参数值,局部变量的值等。

Heap则称之为堆区,由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。

而在C#中:

  Stack是指堆栈,Heap是指托管堆,不同语言叫法不同,概念稍有差别。(此处若有错误,请指正)。

  这里最需要搞清楚的是在语言中stack与heap指的是内存中的某一个区域,区别于数据结构中的栈(后进先出的线性表),堆(经过某种排序的二叉树)。

  讲一个概念之前,首先要说明它所处的背景。

  若无特别说明,这篇文章讲的堆栈指的就是Stack,托管堆指的就是Heap

二,C#堆栈的工作方式

  Windwos使用虚拟寻址系统,把程序可用的内存地址映射到硬件内存中的实际地址,其作用是32位处理器上的每个进程都可以使用4GB的内存-无论计算机上有多少硬盘空间(在64位处理器上,这个数字更大些)。这4GB内存包含了程序的所有部份-可执行代码,加载的DLL,所有的变量。这4GB内存称为虚拟内存。

  4GB的每个存储单元都是从0开始往上排的。要访问内存某个空间存储的值。就需要提供该存储单元的数字。在高级语言中,编译器会把我们可以理解的名称转换为处理器可以理解的内存地址。

  在进程的虚拟内存中,有一个区域称为堆栈,用来存储值类型。另外在调用一个方法时,将使用堆栈复制传递给方法的所有参数。

  我们注意一下C#中变量的作用域,如果变量a在变量b之前进入作用域,b就会先出作用域。看下面的例子:

{ int a; //do something { int b; //do something } }

  声明了a之后,在内部代码块中声明了b,然后内部代码块终止,b就出了作用域,然后a才出作用域。在释放变量的时候,其顺序总是与给它们分配内存的顺序相反,后进先出,是不是让你想到了数据结构中的栈(LIFO--Last IN First Out)。这就是堆栈的工作方式。

  我们不知道堆栈在地址空间的什么地方,其实C#开发是不需要知道这些的。

  堆栈指针,一个由操作系统维护的变量,指向堆栈中下一个自由空间的地址。程序第一次运行时,堆栈指针就指向为堆栈保留的内存块的末尾。

  堆栈是向下填充的,即从高地址向低地址填充。当数据入栈后,堆栈指针就会随之调整,指向下一个自由空间。我们来举个例子说明。

  如图,堆栈指针800000,下一个自由空间是799999。下面的代码会告诉编译器需要一些存储单元来存储一个整数和一个双精度浮点数。

{ int a=1; double b = 1.1; //do something }

  这两个都是值类型,自然是存储在堆栈中。声明a赋值1后,a进入作用域。int类型需要4个字节,a就存储在799996~799999上。此时,堆栈指针就减4,指向新的已用空间的末尾799996,下一个自由空间为799995。下一行声明b赋值1.1后,double需要占用8个字节,所以存储在799988~799995上,堆栈指针减去8。

  当b出作用域时,计算机就知道这个变量已经不需要了。变量的生存期总是嵌套的,当b在作用域的时候,无论发生什么事情,都可以保证堆栈指针一直指向存储b的空间。

  删除这个b变量的时候堆栈指针递增8,现在指向b曾经使用过的空间,此处就是放置闭合花括号的地方。然后a也出作用域,堆栈指针再递增4。

  此时如果放入新的变量,从799999开始的存储单元就会被覆盖了。

二,托管堆的工作方式

  堆栈有灰常高的性能,但要求变量的生命周期必须嵌套(后进先出决定的),在很多情况下,这种要求很过分。。。通常我们希望使用一个方法来分配内存,来存储一些数据,并在方法退出后很长的一段时间内数据仍是可用的。用new运算符来请求空间,就存在这种可能性-例如所有引用类型。这时候就要用到托管堆了。

  如果看官们编写过需要管理低级内存的C++代码,就会很熟悉堆(heap),托管堆与C++使用的堆不同,它在垃圾收集器的控制下工作,与传统的堆相比有很显著的性能优势

  托管堆是进程可用4GB的另一个区域,我们用一个例子了解托管堆的工作原理和为引用数据类型分配内存。假设我们有一个Customer类。

1 void DoSomething() 2 { 3 Customer john; 4 john = new Customer();5 }

  

  第三行代码声明了一个Customer的引用john,在堆栈上给这个引用分配存储空间,但这只是一个引用,而不是实际的Customer对象。john引用包含了存储Customer对象的地址-需要4个字节把0~4GB之间的地址存储为一个整数-因此john引用占4个字节。

  第四行代码首先分配托管堆上的内存,用来存储Customer实例,然后把变量john的值设置为分配给Customer对象的内存地址。

  Customer是一个引用类型,因此是放在内存的托管堆中。为了方便讨论,假设Customer对象占用32字节,包括它的实例字段和.NET用于识别和管理其类实例的一些信息(可以忽略这句)。为了在托管堆中找到一个存储新Customer对象的存储位置,.NET运行库会在堆中搜索一块连续的未使用的32字节的空间,假定其起始地址是200000。

  john引用占堆栈的799996~799999位置。实例化john对象前内存应该是这样,如图。

  给Customer对象分配空间后,内存内容如图。这里与堆栈不同,堆上的内存是向上分配的,所有自由空间都在已用空间的上面。

  以上例子可以看出,建议引用变量的过程比建立值变量的过程复杂的多,且不能避免性能的降低-.NET运行库需要保持堆的信息状态,在堆添加新数据时,这些信息也需要更新(这个会在堆的垃圾收集机制中提到)。尽管有这么些性能损失,但还有一种机制,在给变量分配内存的时候,不会受到堆栈的限制:

  把一个引用变量a的值赋给另一个相同类型的变量b,这两个引用变量就都引用同一个对象了。当变量b出作用域的时候,它会被堆栈删除,但它所引用的对象依然保留在堆上,因为还有一个变量a在引用这个对象。只有该对象的数据不再被任何变量引用时,它才会被删除。

  这就是引用数据类型的强大之处,我们可以对数据的生存周期进行自主的控制,只要有对数据的引用,该数据就肯定存于堆上。

三,托管堆的垃圾收集

  对象不再被引用时,会删除堆中已经不再被引用的对象。如果仅仅是这样,久而久之,堆上的自由空间就会分散开来,给新对象分配内存就会很难处理,.NET运行库必须搜索整个堆才能找到一块足够大的内存块来存储整个新对象。

  但托管堆的垃圾收集器运行时,只要它释放了能释放的对象,就会压缩其他对象,把他们都推向堆的顶部,形成一个连续的块。在移动对象的时候,需要更新所有对象引用的地址,会有性能损失。但使用托管堆,就只需要读取堆指针的值,而不用搜索整个链接地址列表,来查找一个地方放置新数据。

  因此在.NET下实例化对象要快得多,因为对象都被压缩到堆的相同内存区域,访问对象时交换的页面较少。Microsoft相信,尽管垃圾收集器需要做一些工作,修改它移动的所有对象引用,导致性能降低,但这样性能会得到弥补。

四,装箱与拆箱

  有了上面的知识做铺垫,看下面一段代码

int i = 1; object o = i;//装箱 int j = (int)o;//拆箱

  int i=1;在堆栈中分配了一个4个字节的空间来存储变量 i 。

  object o=i;

  装箱的过程: 首先在堆栈中分配一个4个字节的空间来存储引用变量 o,

  然后在托管堆中分配了一定的空间来存储 i 的拷贝,这个空间会比 i 所占的空间稍大些,多了一个方法表指针和一个SyncBlockIndex,并返回该内存地址。

  最后把这个地址赋值给变量o,o就是指向对象的引用了。o的值不论怎么变化,i 的值也不会变,相反你 i 的值变化,o也不会变,因为它们存储在不同的地方。

  int j=int(o);

  拆箱的过程:在堆栈分配4字节的空间保存变量J,拷贝o实例的值到j的内存,即赋值给j。

  注意,只有装箱的对象才能拆箱,当o不是装箱后的int型时,如果执行上述代码,会抛出一个异常。

  这里有一个警告,拆箱必须非常小心,确保该值变量有足够的空间存储拆箱后得到的值。

long a = 999999999; object b = a; int c = (int)b;

  C#int只有32位,如果把64位的long值拆箱为int时,会产生一个InvalidCastExecption异常。

浅谈C#堆栈与托管堆的工作方式(转)的更多相关文章

  1. C#堆栈和托管堆

    首先堆栈和堆(托管堆)都在进程的虚拟内存中.(在32位处理器上每个进程的虚拟内存为4GB) 堆栈stack 堆栈中存储值类型. 堆栈实际上是向下填充,即由高内存地址指向低内存地址填充. 堆栈的工作方式 ...

  2. 浅谈c语言中的堆

    操作系统堆管理器管理: 堆管理器是操作系统的一个模块,堆管理内存分配灵活,按需分配. 大块内存: 堆内存管理者总量很大的操作系统内存块,各进程可以按需申请使用,使用完释放. 程序手动申请&释放 ...

  3. 浅谈SDN架构下的运维工作

    导读 目前国内的网络运维还处于初级阶段,工作人员每天就像救火一样,天天疲于奔命.运维人员只能埋头查找系统运行的日志,耗时耗力,老眼昏花不说,有时候忙了半天还一无所获,作为运维工程师的你,有木有遇到过类 ...

  4. 浅谈Entity Framework中的数据加载方式

    如果你还没有接触过或者根本不了解什么是Entity Framework,那么请看这里http://www.entityframeworktutorial.net/EntityFramework-Arc ...

  5. 浅谈Java中的System.gc()的工作原理

    很多人把Java的“效率低下”归咎于不能自由管理内存,但我们也知道将内存管理封装起来的好处,这里就不赘述. Java中的内存分配是随着new一个新的对象来实现的,这个很简单,而且也还是有一些可以“改进 ...

  6. iOS 浅谈MVC设计模式及Controllers之间的传值方式

    1.简述你对MVC的理解? MVC是一种架构设计.它考虑了三种对象:Model(模型对象).View(试图对象).Controller(试图控制器) (1)模型:负责存储.定义.操作数据 (2)视图: ...

  7. 浅谈Spring解决循环依赖的三种方式

    引言:循环依赖就是N个类中循环嵌套引用,如果在日常开发中我们用new 对象的方式发生这种循环依赖的话程序会在运行时一直循环调用,直至内存溢出报错.下面说一下Spring是如果解决循环依赖的. 第一种: ...

  8. 浅谈Asp.Net中的几种传值方式

    一.使用Querystring Querystring是一种非常简单的传值方式,其缺点就是会把要传送的值显示在浏览器的地址栏中,并且在此方法中不能够传递对象.如果你想传递一个安全性不是那么太重要或者是 ...

  9. 《浅谈F5健康检查常用的几种方式》—那些你应该知道的知识(二)

    版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/sinat_17736151/articl ...

随机推荐

  1. tensorflow 代码阅读

    具体实现: https://github.com/tensorflow/tensorflow/tree/master/tensorflow/core/framework 『深度长文』Tensorflo ...

  2. mac 版本navicate 如何安装破解版

    https://www.jianshu.com/p/f42785e55b6b  博客地址 部分童鞋安装后没有rpk文件,我也不知道怎么解决实在不行,请下载破解版链接:https://pan.baidu ...

  3. gitlab 添加 ssh

    git 客户端无法拉取gitlab仓库代码,登陆网页端,查看仓库主页有下面的提示 因此需要添加ssh公钥,才能上传下拉代码 windows平台: 首先需要安装git了. 在存放代码的目录中,右键选择 ...

  4. Ubuntu下重新安装软件 配置文件不重新生成得问题解决

    apt-get remove nfs dpkg -P nfs apt-get install nfs 按照先remove然后dpkg -P再重新install的顺序.

  5. Unity 3D入门简介

    最近在刚开始学习Unity 3D,在这里记录一下学习心得和学习笔记,边学边写,可能会比较零散.好了,废话不多说,今天从Unity 3D入门写起,主要简要介绍一下Unity 3D的和一些学习资料.以下如 ...

  6. 使用java注解实现toJson方法

    如果我有一个对象user,它有几个属性,我想把该对象序列化成一个json字符串,怎么做?我怎么把这种类型的问题实现成一个函数? 注解类似于在被注解的对象上,添加一些简单的属性.在运行时解析这些属性,以 ...

  7. python基础知识2---核心风格

    阅读目录 一.语句和语法 二.变量定义与赋值 三.内存管理 内存管理: 引用计数: 简单例子 四.python对象 五.标识符 六.专用下划线标识符 七.编写模块基本风格 八.示范 一.语句和语法 # ...

  8. PAT 乙级 1074 宇宙无敌加法器 (20 分)

    1074 宇宙无敌加法器 (20 分) 地球人习惯使用十进制数,并且默认一个数字的每一位都是十进制的.而在 PAT 星人开挂的世界里,每个数字的每一位都是不同进制的,这种神奇的数字称为“PAT数”.每 ...

  9. spring 事物不回滚

    使用spring控制事物,为什么有些情况事物,事物不回滚呢?? 默认spring事务只在发生未被捕获的 RuntimeException时才回滚.   spring aop  异常捕获原理: 被拦截的 ...

  10. Logic and Proofs--离散数学

    Propositions: A proposition is a declarative sentence(that is, a sentence that declares a fact ) tha ...