在一次偶然的机会中,我来到了恒生的你们庭。又在一次偶然的机会中,我很荣幸的被勇哥信任并让我写一篇季刊的文章。可能人生之中充满了无数次的偶然机会,咱们只有抓住眼前的“偶然”,才能够建立人生。当我接到这个任务的时候,有一些激动又有一些惧怕。激动的是我又有机会去分享本身知道的知识了,可是仍是有些惧怕,在恒生中大牛们太多了,写什么其实都是没有什么技术含量的。在激动和压力之中,最终仍是写下了这一篇文章。用此文章来激励本身和那些当初选择了c#而转行的兄弟们。也许有一些地方说的不是很正确,但愿读者不吝赐教。个人我的邮箱是:codeany@163.com。html
读者类型:java
这篇文章适应的读者为:未学习过c#、刚学c#、或者从未系统的学习过c#底层的程序员。c++
专业术语解析:程序员
GC堆:Garbage Collection,在c#中,当没有变量指向一块GC堆的内存时,他不会当即把这块内存回收,而是等到系统在合适的时间去回收这一块内存。编程
LOH堆:Large Object Heap,用于分配大对象实例。若是引用类型对象的实例大小不小于85000字节时,该实例将被分配到LOH堆上,不一样于GC堆,垃圾收集器不会对LOH堆进行压缩。c#
类型句柄:TypeHandle,TypeHandle指向相关连的类型的MethodTable。任何一个声明了的类型都仅有一个MethodTable, 而且全部一样类型对象的实例都指向同一份MethodTable。windows
SyncBlockIndex:指针指向Synchronization Block的内存块,用于在多线程环境下对实例对象的同步操做。数组
IL代码:Intermediate Language,IL是.NET框架中中间语言的缩写。在.NET中支持的语言有C#、VB、F#等等,可是这些高级语言,最终生成IL代码,最后经过IL代码解析器解析,从而实现多语言的开发。多线程
拆箱装箱:在c#中,若是把值类型转化为引用类型叫作装箱,反之若是把引用类型转化为值类型叫作拆箱。装箱和拆箱的操做都是很是耗性能的,全部咱们平时编程的时候尽可能避免装箱拆箱的操做。框架
简单的new提及
提及 new 我想你们并不会陌生,虽然我没有去深刻的学习过java、c、c++的new,可是我想在这里和你们分享一下我自个认为的c#的new。在new以前咱们仍是先建立一个class类,对于程序员来讲,最好的解释莫过于代码,那我就直接看代码吧:
/// <summary> /// 定义一个用户信息的类 /// </summary> public class UserInfo { public Int32 age = 12; // 用户的年龄 public char sex = 'M'; // 用户的性别 } /// <summary> /// 用来定义一个用户类 /// </summary> public class Person { public Int32 id = 09397; // 用户的id号 public UserInfo user; // 用户信息 } /// <summary> /// 从用户类中继承获得一个恒生用户类 /// </summary> public class HsPerson : Person { public bool isGood = true; // 是不是优秀 这里默认是优秀的 }
我想上面的代码你们必定不会陌生,并且确定每一个人都会有多多少少写过相似的代码。那咱们今天也就从这些代码转入咱们的正题。首先咱们能够考虑一个问题,咱们本身建立的类占用了多少内存,而且在内存中是如何分配的。好吧,我想有的人必定会说这个还不简单,直接调用sizeof来计算占用空间大小不就解决问题了。其实当初我也是这么想的,可是很是遗憾的告诉你,在c#中sizeof是计算值类型大小的。可是咱们本身建立的类是引用类型。因此问题并不会这么简单。可是咱们能够手动的计算获得咱们须要的答案。在计算以前咱们先来看一下什么叫值类型、引用类型。
值类型与引用类型(参考微软的MSDN):
值类型:
若是数据类型在它本身的内存分配中存储数据,则该数据类型就是“值类型”。 值类型包括:
每一个结构是值类型,所以,即便它包含引用类型成员。 所以,值类型 (如 Char 和 Integer 由 .NET framework 结构实现。
能够经过使用保留关键字(例如 Decimal)声明值类型。 也可使用 New 关键字初始化值类型。 这对于值类型有一个带参数的构造函数的状况尤其有用。此示例有 Decimal(Int32, Int32, Int32, Boolean, Byte) 构造函数,它从提供的部分生成新的 Decimal 值。
引用类型:
“引用类型”包含指向存储数据的其余内存位置的指针。 引用类型包括:
类是一种“引用类型”。 所以,诸如 Object 和 String 之类的引用类型都受 .NET Framework 类支持。 请注意,每一个数组都是一种引用类型,即便其成员是值类型。
在了解了值类型和引用类型以后,咱们回归到咱们的本文的正题。值类型的变量保存到内存的线程的堆栈中;而引用类型的变量会保存到托管堆中,其中这里说的托管堆又能够分为GC堆、LOH堆。其中GC堆、LOH堆是根据建立的对象的大小来分配到不一样的堆中的,判断的平衡点是这个对象是否超过85000字节,若是小于85000字节,则系统把对象保存到GC堆中;若是大于或者等于85000字节,则系统保存到LOH堆中(通常LOH建立的对象是数组)。因此咱们常说的托管堆就是指GC堆和LOH堆的集合。固然,我这里写的也不是彻底正确的,其实在c#中建立对象是一个很是复杂过程,当中会涉及到系统程序域、共享程序域和默认程序域等等。我这么写只是想把问题简单化,让更多地人先了解一个大概的过程。
接下来咱们来开始计算一下第一个问题——对象占用了多少空间,在UserInfo中咱们定义了一个int32的age(4byte),一个char的sex(2byte),在Person中咱们定义了int32的id(4byte),指针类型的user(4byte)类型,在HsPerson中咱们继承了Person类,并添加了bool的isGood(1byte),因此咱们一共占用了4+2+4+4+1=15byte。其实计算并无结束,实例对象所占字节数还要加上对象附加成员所需的字节总数,其中附加成员包括TypeHandle和SyncBlockIndex,共计8Byte(在32位CPU平台下面),因此咱们一共占用了23byte字节。然而在堆上分配的内存老是按照4byte的整数倍,因此最后咱们能够得知咱们在GC堆上要了24byte空间(咱们建立的对象通常是不会超过85000byte字节的,全部大部分都是在GC堆中,有一些特别大的数组byte[85000],此事CLR就会把它保存到LOH堆中)。第二个问题解答从里氏替换原则图中找答案吧!
里氏替换内存管理
在上面的类的基础上,咱们来建立几个对象,来看看内存是怎么分配的。
static void Main(string[] args) { // 完成一个简单的 class 操做 HsPerson hs = new HsPerson(); hs.user = new UserInfo(); Console.WriteLine(@"显示信息1: 用户{0}优秀 用户id号是{1} 用户年龄是{2} 用户性别是{3} ",hs.isGood==true?"是":"不是",hs.id,hs.user.age,hs.user.sex); // 引入一个难点 --氏替换原则 Person hsperson = hs; //下面这样子打印会出错 /*Console.WriteLine(@"显示信息2: 用户{0}优秀 用户id号是{1} 用户年龄是{2} 用户性别是{3}", hsperson.isGood == true ? "是":"不是",hsperson.id, hsperson.user.age, hsperson.user.sex);*/ Console.WriteLine(@"显示信息2: {0} 用户id号是{1} 用户年龄是{2} 用户性别是{3} ", "这里因为变量hsperson指向变量范围的控制,读取不到isGood 字段", hsperson.id, hsperson.user.age, hsperson.user.sex); Console.ReadKey(); }
图 里氏替换内存分配图
这里有一点但愿你们不要误解,就是我把Person类放到了HsPerson类中,这样只是方便的让读者更好地理解里氏替换原则的一些细节(实际上Person类也是在一块连续的GC堆中)。咱们仍是一边从代码分析,一边从图分析,首先咱们声明了一个hs类型为HsPerson的变量,这个变量会在线程堆栈中占用4byte的空间,紧接着咱们用new在GC堆中建立了一个HsPerson的实例,其实咱们用“=”把这个建立的对象的实例的地址告诉了变量hs,此时变量已经初始化完成了。紧接着咱们建立了一个hsperson类型为Person的类,而且从hs的变量中把地址拿过来也做为本身的对象的地址。可是奇怪的问题发生了,咱们不能够用hsperson去调用isGood这个字段。为何呢?其实这个是变量的偏移量在搞鬼,当咱们建立变量的同时已经告诉了这个对象他能够有多少的偏移量,hs他具备访问全部的HsPerson这个对象的偏移量,而hsperson他只具备访问在HsPerson中的Person的偏移量。可能你还不是很明白,那我这里举一个例子,假如咱们有一根1米的棒子,去摘树上的桃子,那么咱们就只能摘到1米如下的桃子,而当咱们去摘1米以上的桃子时,此时编译器就会报错,告诉咱们超越咱们能力了。(这里咱们去掉了我的身高也去掉了有些特殊的方法去摘桃),因此当咱们想要摘到全部的桃子,咱们只须要有一根最高桃子树高度的棒子就能够了。可是这样你的变量可能拥有了所有的访问权限,可是在面向对象的编程之中,咱们提倡基于接口编程,这样只须要对象访问到本身高度的桃子并可以完成任务就能够了。 固然里氏替换用的最为普遍的应该是动态的调用对象了(因为不是本文重点,就不一一展开了)。
原本还想写一些IL代码分析内存、拆箱装箱内存知识,因为时间有限,就先写到这里,若是你该兴趣,我能够和你在私下交流。写了这么多,我只是想告诉你在c#中对象的建立是个复杂的过程,主要包括内存分配和初始化两个环节。
参考:
http://www.cnblogs.com/anytao/
http://msdn.microsoft.com/zh-tw/library/dd229211.aspx
http://www.cnblogs.com/artech/archive/2010/10/20/CLR_Memory_Mgt_01.html