zoukankan      html  css  js  c++  java
  • 大话 .Net 之内存管理

    在一次偶然的机会中,我来到了恒生的大家庭。又在一次偶然的机会中,我很荣幸的被勇哥信任并让我写一篇季刊的文章。可能人生之中充满了无数次的偶然机会,我们只有抓住眼前的“偶然”,才可以创建人生。当我接到这个任务的时候,有一些激动又有一些害怕。激动的是我又有机会去分享自己知道的知识了,但是还是有些害怕,在恒生中大牛们太多了,写什么其实都是没有什么技术含量的。在激动和压力之中,最终还是写下了这一篇文章。用此文章来激励自己和那些当初选择了c#而转行的兄弟们。也许有一些地方说的不是很正确,希望读者不吝赐教。我的个人邮箱是:codeany@163.com。

    读者类型:

    这篇文章适应的读者为:未学习过c#、刚学c#、或者从未系统的学习过c#底层的程序员。

    专业术语解析:

           GC堆:Garbage Collection,在c#中,当没有变量指向一块GC堆的内存时,他不会立即把这块内存回收,而是等到系统在合适的时间去回收这一块内存。

           LOH堆:Large Object Heap,用于分配大对象实例。如果引用类型对象的实例大小不小于85000字节时,该实例将被分配到LOH堆上,不同于GC堆,垃圾收集器不会对LOH堆进行压缩。

    类型句柄:TypeHandle,TypeHandle指向相关连的类型的MethodTable。任何一个声明了的类型都仅有一个MethodTable, 并且所有同样类型对象的实例都指向同一份MethodTable。

    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):

    值类型:

    如果数据类型在它自己的内存分配中存储数据,则该数据类型就是值类型。 值类型包括:

    • 所有数字数据类型
    • Boolean 、Char 和 Date
    • 所有结构,即使其成员是引用类型
    • 枚举,因为其基础类型总是 SByteShortIntegerLongByteUShortUInteger 或 ULong

    每个结构是值类型,因此,即使它包含引用类型成员。 因此,值类型 (如 Char 和 Integer 由 .NET framework 结构实现。

    可以通过使用保留关键字(例如 Decimal)声明值类型。 也可以使用 New 关键字初始化值类型。 这对于值类型有一个带参数的构造函数的情况尤为有用。此示例有 Decimal(Int32, Int32, Int32, Boolean, Byte) 构造函数,它从提供的部分生成新的 Decimal 值。

     

    引用类型:

    引用类型包含指向存储数据的其他内存位置的指针。 引用类型包括:

    • String
    • 所有数组,即使其元素是值类型
    • 类类型,如 Form
    • 委托

    类是一种“引用类型”。 因此,诸如 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/dudu/

    http://www.cnblogs.com/artech/archive/2010/10/20/CLR_Memory_Mgt_01.html

    http://msdn.microsoft.com/zh-cn/library/x9h8tsay.aspx

    http://msdn.microsoft.com/zh-cn/library/t63sy5hs

     

  • 相关阅读:
    Windows Embedded CE 中断结构分析
    linux framebuff驱动总结
    Linux assemblers: A comparison of GAS and NASM
    使用C#编写ICE分布式应用程序
    makefile的写法
    在客户端中如何定位服务器(即如何寻找代理)
    番茄花园洪磊: 微软很早给我发过律师函
    利用ICE编写程序的几个注意点
    ICE架构
    AT&T汇编语言与GCC内嵌汇编简介
  • 原文地址:https://www.cnblogs.com/Jimmy009/p/3227014.html
Copyright © 2011-2022 走看看