zoukankan      html  css  js  c++  java
  • .NET 内存基础(通过内存体验类型、传参、及装箱拆箱)

    该随笔受启发于《CLR Via C#(第三版)》第四章4.4运行时的相互联系

    一、内存分配的几个区域

    1、线程栈

    局部变量的值类型  局部变量中引用类型的指针(或称引用)会被分配到该区域上(引用类型的一部分内存被分配到该区域内)。

    该区域由系统管控,不受垃圾收集器的控制。当所在方法执行完毕后,局部变量会自动释放(引用类型只释放指针,而不释放指针指向的数据)

    堆栈的执行效率很高,但容量有限。

    2GC Heap(回收堆)

    用于分配小对象(引用类型),如果引用类型的实例大小 小于85000个字节,则会被分配到该区域上。

    3LOH(Large Object Heap)

    超过 85000个字节的大对象(引用类型)会被分配到该区域上。

    LOH GC Heap区别在于:当有内存分配或者回收时,垃圾收集器可能会对GC Heap进行压缩,而 LOH 不会被压缩,只是在垃圾回收时被回收。

    二、栈桢(stack frame)

    栈桢是实现函数调用的数据结构,从逻辑上看,栈桢是一个函数执行的环境(上下文),包括:函数参数、返回地址和函数局部变量。

    注意:上述的“返回地址” 不是函数的返回值,而是完成函数调用后的CPU接下来要执行的代码的位置。

    更多参见: 维基百科 call stack 

    三、浅拷贝 和 深拷贝(Clone、或克隆)

    浅拷贝和深拷贝是内存拷贝的两种方式,在传参的过程中通常会发生内存拷贝并且是浅拷贝,比如下面代码、两个参数就是两次浅拷贝。

            public void Test1()
            {
                string name = "test1";
                int size = 1;
                Test2(name, size);// 两次浅拷贝
            }
    
            public void Test2(string pName, int pSize)
            {
            }
    

     浅拷贝 前后的内存结果如下图所示:

    浅拷贝对于值类型(例如:int),拷贝的是栈中的具体值。

    浅拷贝对于引用类型(例如:string),拷贝的是栈中的引用、其新引用 所指向的 托管堆中的地址 还是原来的地址。

    深拷贝和浅拷贝的区别在于对 堆中数据(即对象)的处理

    浅拷贝只拷贝栈中的引用,不拷贝堆中的对象,新引用指向原有对象。

    深拷贝会拷贝栈中的引用,并且会在堆中创建新的对象,新对象的属性值 “可能” 来源于原有对象(因为、在C#中实现深拷贝需要自己编写额外的代码,

    即实现 ICloneable 接口,深拷贝的结果是不确定的。不过建议大家尽可能避免使用该接口),新引用地址指向新的对象。因此,深拷贝只针对于引用类型,

    对于值类型没有太多的意义。

    四、Demo

    如果说前三项是做铺垫,那么该Demo算是正文了。

    事先已经定义好的如下类:

        public class Data 
        {
            public string Name;
            public int Size;
        }
    

    假设代码即将调用Test1函数:

            public void Test1()
            {
                string name = "test1";
                int size = 1;
    
                Data data = new Data() { Name = "test1", Size = 1 };
    
                Test2(name, size, data);
    
                object obj = size;
                int temp = (int)obj;
            }
    
            public void Test2(string pName, int pSize, Data pData)
            {
                pName = "test2";
                pSize = 2;
    
                pData.Name = "test2";
                pData.Size = 2;
                
                pData = new Data() { Name = "test3", Size = 3 };            
            }
    

    此时线程栈和托管堆内的情况如下图所示(图1):

    1、开始调用Test1函数,这时会向栈中压入一个栈桢(stack frame)。

      栈桢包含3部分数据:

      1)函数参数,当然Test1函数没有参数。

      2)返回地址,在Test1函数中,以当前代码环境为例该地址没有什么实际意义不作说明。

      3)函数局部变量,此时Test1函数中的所有的局部变量都会被压入栈。 有如下五个局部变量会被压入栈:

    string name, int32 size, Data data, object obj, int32 temp。(当然实际是六个局部变量,第六个是编译器自动生成的,在随笔结尾处再做解释。)

     压入一个栈桢后,内存结果如下图所示(图2):

    2、接下来执行Test1函数的代码,给name赋值,给size赋值,给data赋值。内存结果将会如下图所示(图3):

    上图中的红线编译器生成的变量 将在随笔结尾出做解释。

    3、接下来将调用Test2函数,这时还会向栈中压入一个栈桢。

    栈桢包含3部分数据:

      1)函数参数,Test2函数有三个参数,三个参数都是浅拷贝。

      2)返回地址,该地址为调用Test2函数代码位置的下一行(或者称下一个指令)的位置,即:

    obj = size; // 在调用Test1函数压入栈桢的时候已经完成了 object obj; 的操作

      完成函数调用后,将执行上述代码。

      3)函数局部变量,编译器会自动生成一个变量(随笔结尾处,将做出解释)

    故,此时的内存结果如下图所示(图4):

    两条橙色线是参数浅拷贝的结果。

    4、 在执行完Test2函数的函数体,未返回Test1函数之前,内存结果如下图所示(图5):

    红线 将在随笔结尾处做解释。

    黄线 需要注意,string是特殊的引用类型,其外在表现和值类型一致,但其本质上还是引用类型。由于string的不可变性,对string的每一次赋值

      都会产生一个新的对象(如果新对象不存在),所以导致了黄线的出现。

    浅绿线 我想这条线应该没有问题吧。

    5、 完成Test2函数的调用,代码将返回到 Test2函数栈桢返回地址所示位置,在这一过程中 Test2函数栈桢 将被弹出,结果如下图所示(图6):

    托管堆中的不被使用的对象将由GC进行自动回收,被回收的时间不确定。

    6、 由3可以知,接下来将执行 obj = size; 的操作——装箱,从字面上可以这样理解:将值类型装进引用类型的箱子里。

    大致的操作过程如下:

      1)首先、在堆中创建一个新的对象。(由于该操作导致装箱操作的性能降低)

      2)将size的值复制到对象中。

      3)最后将obj的引用指向新创建的对象。

    最后的结果如下图所示(图7):

    7、最后一个操作——拆箱

    装箱,是将值类型装进引用类型的箱子里,在这一过程中,发生对象创建和内存复制。 

    然而拆箱,并没有那么复杂,也没有类似的“拆”的过程,只是将对象中的值类型读取出来(最耗时的操作应该是寻找值类型所在的内存地址的操作),相对于装箱,其性能要好很多。

    最终结果如下图所示(图8):

    五、 语法糖——对象初始化器

    如下的两个代码片段是完全等效的:

                Data temp = new Data(); // 编译器自动生成的变量名不一定叫temp
                temp.Name = "test1";
                temp.Size = 1;            
                data = temp;
    
           data = new Data() { Name = "test1", Size = 1 }; 
    

    第二种写法,可以看作是一种简写,编译器在编译的时候会将第二种写法的代码进行转化,转化成第一种写法。所以对象初始化器,C#3.0的语法新特性,

    完全是一种语法糖,CLR没有起到任何作用。

    当看到这的时候,再想想上面的红线 一切迎刃而解。

  • 相关阅读:
    【Nginx】使用Nginx做反向代理时,关于被代理服务器相应的超时设置
    【Quartz】配置最简单的集群
    【Quartz】将定时任务持久化到数据库
    【Quartz】Quartz的搭建、应用(单独使用Quartz)
    【Linux】用grep在文档中查找内容
    【Linux】方便的SecureCRT文件上传、下载命令
    【MySQL】MySQL复制表结构、表数据
    【Linux】vi(vim)起步学起来有些困难,一步一步温习
    【MySQL】MySQL PLSQL Demo
    【Linux】VMware中为CentOS设置静态IP(非动态获取IP)
  • 原文地址:https://www.cnblogs.com/08shiyan/p/3935580.html
Copyright © 2011-2022 走看看