• C# 内存分配


    博文带着3个疑问学习:(整理的有错误,请大家帮我改正)

    问题1:CLR管理内存的三块区域是什么?

    问题2:哪些操作会 创建对象和分配内存?

    问题3:内存的分配机制?

     

    1.CLR管理内存的三块区域
    注:内存——堆栈   堆(托管堆)

    线程的堆栈:用于分配值类型的实例-有操作系统管理分配释放内存。
    GC堆(托管堆):用于分配引用类型的实例对象内存小于8500 byte的。当有内存分配时,垃圾回收器"可能"会对GC堆进行压缩。
    LOH堆(Large Object Heap):用于分配引用类型的大对象实例(大于8500byte),不会被垃圾回收器压缩,而且只在GC堆完全被回收时回收。

    2.哪些操作会 创建对象和分配内存?
    IL指令:
    newobj:引用类型对象创建
    ldstr:string类型对象创建
    newarr:数组对象创建
    box:值类型转换引用类型,值类型字段拷贝到托管堆上发生 内存分配

    3.内存的分配机制
    3.1 堆栈的内存分配机制
    对于值类型:当作为类的值类型成员时,这个时候值类型将被分配在托管堆(堆)上。如:

    class Car
    {
        int carYear;
        string carName;
    }

    复制代码



    即:Car oneCar=new Car();  这个Car类的引用变量将被分配在线程堆栈上,而这个对象的成员,如:carYear,carName将被分配在堆上。
    注意:堆栈-stack  堆-heap

    对于堆栈的变量来说,是由操作系统来分配和释放内存的,操作系统维护着一个堆栈指针来指向一个自由空间(未被分配的内存的开始位)。堆栈的分配是从高位——>低位,而释放是从低位——>高位,如:

    static void Main()
    {
        int i=1;
        char a='A';

    }

    复制代码




    分析:
    分配
    第一步:C#程序从Main函数开始,每个线程堆栈都有一个初始化地址,比如这个程序的初始化地址是100;
    第二步:int类型占有4字节,那么开始堆栈指针是在100这个位置,然后分配4字节给值为1的Int类型(100-97)保存,然后堆栈指针指向96.
    第三步:char类型占有2字节,那么堆栈指针由96指向94。
    第三步:当运行到右括号的时候,将会释放内存
    释放
    第四步:释放的步骤是分配的反方向,堆栈指针逐步向上移动。

    注:上面的方式更可以说是“局部变量的分配机制”,因为这些值类型变量随着方法的结束而结束,效率高,但是内存容量小。
        
    3.2 堆(托管堆)的内存分配机制
    对于引用类型:引用类型的变量的内存是分配在堆栈上的,而引用类型的对象实例的内存是分配在托管堆上的。
    对于托管堆里有2个重要的区域:GC堆和加载堆(Loader Heap)

    GC堆存储对象实例字段,由GC管理。
    Loader堆有High-Frequency Heap、Low-Frequency Heap、Stub Heap,存储的是元数据相关的信息,比如:Type对象,即方法表。方法表创建于编译时,主要包含了类型的特征信息、实现的接口数目、方法表的slot数目等。 Loader堆不受GC控制,生命周期是从创建到AppDomain卸载。

    如下代码:
     

    class Program
        {
            static void Main(string[] args)
            {
                VIPUser vipuser;
                vipuser = new VIPUser();
                vipuser.isVip = true;
                Console.WriteLine(vipuser.IsVip());
            }
        }

        public class UserInfo
        {
            private Int32 age = -1;
            private char level = 'a';

        }
        public class User
        {
            private Int32 id;
            private UserInfo user;
        }
        public class VIPUser : User
        {
            public bool isVip;
            public bool IsVip()
            {
                return isVip;
            }
        }

    复制代码

     



    分析:
    第一步:Main函数开始,声明一个VIPUser类型变量,这只是一个引用,或者说是一个指针,它是存储在堆栈上的,占有4个字节。这个时候,没有指向对象,初始化为NULL。
    第二步:new对象,这个过程很复杂,因为IL代码为newobj,这个过程会一直向上查找其所有父类,直到Object类型,并且这个过程会计算类型和所有继承关系类型的字段,并且返回一个总的占有字节数。
    开始计算如下:
    VIPUser类的bool类型 (1字节)+User类的[Int32类型(4字节)+UserInfo类型的user的引用(4字节)]+UserInfo类的[Int32类型的(4字节)+char类型的(2字节)]=1+8+6=15字节。

    注意:在32位系统下,TypeHandle和SyncBlockIndex附加成员也会占有8字节。即:一共是23字节,但是托管堆上一般是按4字节的倍数分配的,所以会分配24字节的内存。

    注意:NextObjPtr是一个神奇的指针,他在托管堆中,标识下一个新建对象在托管中的位置,并且会返回对象实例的内存地址给 堆栈中的引用变量。
    第三步:分配内存
    对于堆栈是 先进后出,向低位扩展 ,分配内存是由上-下,释放内存是由下-上。
    对于堆是 先进先出,向高位扩展。分配内存是由下-上,由GC回收器回收内存。

    对于引用类型,父类在前子类在后,当发现内存不足时,会启动GC回收器,回收垃圾对象占用的内存。
    第四步:调用构造函数,进行初始化,完成创建对象过程
    如下:
    .构造VIPUser类型的Type对象,主要包括静态字段、方法表、实现的接口等,并将其分配在上文提到托管堆的Loader Heap上。 
    .初始化vipuser的两个附加成员:TypeHandle和SyncBlockIndex。将TypeHandle指针指向Loader Heap上的MethodTable,CLR将根据TypeHandle来定位具体的Type;将SyncBlockIndex指针指向Synchronization Block的内存块,用于在多线程环境下对实例对象的同步操作。 
    .调用VIPUser的构造器,进行实例字段的初始化。实例初始化时,会首先向上递归执行父类初始化,直到完成System.Object类型的初始化,然后再返回执行子类的初始化,直到执行VIPUser类为止。以本例而言,初始化过程为首先执行System.Object类,再执行User类,最后才是VIPUser类。最终,newobj分配的托管堆的内存地址,被传递给VIPUser的this参数,并将其引用传给栈上声明的vipuser

    上一篇博文也说了一点关于内存的知识,但是不详尽,这篇博文彻底的理解了从.net层面理解  引用类型的内存分配--->引用类型的堆内的工作,以及继承的本质。

    (说的不对,大家指正)

     

    继承
    面向对象:实现单继承和接口多继承
    对于.net通过访问权限的修饰符控制安全:public protected internal private
    抽象方法和虚方法才可以被重写override,而且虚方法不能是private才可以被重写,抽象方法必须是public.
    接口的默认是公共抽象的方法,而且被继承了,必须被实现。


    1.继承机制的执行

    View Code

    public abstract class Animal
        {
            public abstract void ShowType();
            public void Eat()
            {
                Console.WriteLine("Animal always eat.");
            }
        }
        public class Bird : Animal
        {
            private string type = "Bird";
            public override void ShowType()
            {
                Console.WriteLine("Type is {0}", type);
            }
            private string color;
            public string Color
            {
                get { return color; }
                set { color = value; }
            }
        }
        public class Chicken : Bird
        {
            private string type = "Chicken";
            public override void ShowType()
            {
                Console.WriteLine("Type is {0}", type);
            }
            public void ShowColor()
            {
                Console.WriteLine("Color is {0}", Color);
            }
        }

        public class TestInheritance
        {
            public static void Main()
            {
                Bird bird = new Bird();
                Chicken chicken = new Chicken();
            }
        }

    复制代码


     

    分析:
    1.程序入口依旧是 Main函数,当 Bird bird;时,堆栈中为bird分配4字节的内存存储指针指向堆中的对象实例地址。
    2.new Bird()时,如下:
    2.1先计算需要在堆中分配的内存,计算内存是从子类开始---基类结束。
    2.2调用构造函数,构造Bird类型的Type对象,静态方法,方法表,接口等,分配在Load Heap上,所以方法表式优先于对象分配内存的。
    只有用到的类的方法表才会被加载进Load堆。
    注:任何类型方法表中,开始的4个方法总是继承自System.Object类型的虚方法,它们是:ToString、Equals、GetHashCode和Finalize。
    2.3 初始化Bird的两个附加成员:TypeHandle和SyncBlockIndex。
    2.4调用构造器,进行实例字段的初始化,初始化的过程是从基类-父类-子类。(由上一篇图可知,是向高位扩展的方式,所以附件成员在下,字段在上)
    字段的排序是:父类的字段在子类的字段前面,如果同名编译器会认为是不同的字段。
    2.5 IL代码newobj分配内存的地址将传递给引用变量:bird变量。

    注意:方法表是在类第一次加载到AppDomain完成的,而且生命周期是直到AppDomain被卸载。
    如果再有新的对象实例被创建,只是将对象的附件成员TypeHandle指向方法列表的Load Heap地址上。

    3.当Chicken chicken时,大致是一样的,但是会把父类的方法复制一份,然后与自己的方法列表比较,是否覆盖
    4. new Chicken()时,同上。

    注意:而且为对象实例分配内存,堆中式由下而上的,向高位扩展,类似于NextObjPtr指针是向上移动的。而在堆栈中,堆栈指针式向下移动的。
    在回收内存时,必须GC堆被回收后才会回收Load堆。


    结论:对于堆栈,是向低位扩展,所以指针是向下移动的。
    对于GC堆,对象的实例是向高位扩展的,所以指针是向上移动,但是对于初始化实例,存储过程是在对象所占内存中是由上到下,即在GC堆内部父类字段在前,子类字段在后。
    对于Load堆是属于堆的,所有也是从低位向高位扩展存储分配,对于Load堆内部的方法表父类的方法在前,子类的方法在后。任何类型方法表中,开始的4个方法总是继承自System.Object类型的虚方法,它们是:ToString、Equals、GetHashCode和Finalize。
     
    - 继承是可传递的,子类是对父类的扩展,必须继承父类方法,同时可以添加新方法。 
    - 子类可以调用父类方法和字段,而父类不能调用子类方法和字段。 
    - 虚方法如何实现覆写操作,使得父类指针可以指向子类对象成员。 
    - 子类不光继承父类的公有成员,同时继承了父类的私有成员,只是在子类中不被访问。 
    - new关键字在虚方法继承中的阻断作用。 


    疑惑1:Bird bird2 = new Chicken(); 调用方法的时候到底调用哪一个类的方法?子类?父类?
    答:调用子类方法还是父类方法,取决于创建的对象是子类对象还是父类对象,与引用变量的类型无关。引用类型的不同只是决定了不同对象在方法表中的不同访问权限。
    如下代码:

    View Code

    class Program
        {
            static void Main(string[] args)
            {
                P1 p1 = new P2();
                p1.S();//因为是P1类型的所以无法访问P2的方法
                
                P2 p2 = new P2();
                p2.S1();
                p2.S();
            }
        }
        class P1
        {
            public void S()
            {
                Console.WriteLine("我是父类方法");
            }
        }
        class P2 : P1
        {
            public void S1()
            {
                Console.WriteLine("我是子类方法");
            }    
        }

    复制代码



    如果这样声明:
    Bird bird2 = new Chicken(); 
    Chicken chicken = new Chicken(); 
    答:根据上文的分析,bird2对象和chicken对象在内存布局上是一样的,差别就在于其引用指针的类型不同:bird2为Bird类型指针,而chicken为Chicken类型指针。以方法调用为例,不同的类型指针在虚拟方法表中有不同的附加信息作为标志来区别其访问的地址区域,称为offset。不同类型的指针只能在其特定地址区域内执行,子类覆盖父类时会保证其访问地址区域的一致性,从而解决了不同的类型访问具有不同的访问权限问题。


    - 执行就近原则:对于同名字段或者方法,编译器是按照其顺序查找来引用的,也就是首先访问离它创建最近的字段或者方法,例如上例中的bird2,是Bird类型,因此会首先访问Bird_type(注意编译器是不会重新命名的,在此是为区分起见),如果type类型设为public,则在此将返回“Bird”值。这也就是为什么在对象创建时必须将字段按顺序排列,而父类要先于子类编译的原因了。 

    注:也就是我们所说的  引用类型不同决定的是 访问权限问题。

    疑惑2:如果在子类内 new 父类的同名方法会怎么样?
    答:关于new关键字在虚方法动态调用中的阻断作用,也有了更明确的理论基础。在子类方法中,如果标记new关键字,则意味着隐藏基类实现,其实就是创建了与父类同名的另一个方法,在编译中这两个方法处于动态方法表的不同地址位置,父类方法排在前面,子类方法排在后面。 

    Type is Chicken,为什么,这里就没有就近原则?
    如下代码:
     

    View Code

    public abstract class Animal
        {
            public abstract void ShowType();
            public void Eat()
            {
                Console.WriteLine("Animal always eat.");
            }
        }
        public class Bird : Animal
        {
            public string type = "Bird";
            public override void ShowType()
            {
                Console.WriteLine("Type is {0}", type);
            }
            private string color;
            public string Color
            {
                get { return color; }
                set { color = value; }
            }
        }
        public class Chicken : Bird
        {
            public string type = "Chicken";
            public override void ShowType()
            {
                Console.WriteLine("Type is {0}", type);
            }
            public void ShowColor()
            {
                Console.WriteLine("Color is {0}", Color);
            }
        }

        public class TestInheritance
        {
            public static void Main()
            {
                Bird bird2 = new Chicken();
                Console.WriteLine(bird2.type);//bird 就近原则

                bird2.ShowType();//Type is Chicken,为什么,这里就没有就近原则?

            }
        }

    复制代码

    疑惑3:bird2.ShowType();//Type is Chicken,为什么,这里就没有就近原则?
    答:这里创建的是 Chicken对象,所以加载父类Bird方法表和自己的方法表,如果有重写就覆盖,这里覆盖了,所以没有使用就近原则。
    上面 bird2.type,因为没有被覆盖,编译器认为是2个不同的变量,所以就近原则

  • 相关阅读:
    Redis初启(一)
    分布式架构下的通信(一)
    Python学习笔记:本月、上月、次月生成
    Python学习笔记:字符串转换为时间对象
    超级详细的mysql数据库安装指南
    net core 3.1 mvc string接收为null解决方案
    linux 连接跟踪 conntrack
    Cilium 数据链路
    strace/perf trace 使用
    quagga 部署启动BGP
  • 原文地址:https://www.cnblogs.com/mingxuantongxue/p/3821873.html
Copyright © 2020-2023  润新知