.NET Core 类的生命周期

2021-01-14 07:11

阅读:684

标签:内存区域   创建对象   控制   数据信息   info   方法   相关   inf   com   

本节内容为类的生命周期

引言

对象究竟是一个什么东西?对于许多初学者而言,对象都是一个非常抽象的知识点。如果非要用一句话描述,我觉得“万物皆对象”是对于对象最全面的概述了。本节内容中,我们将以在富土康打工的张全蛋组装一台水果手机作为例子,详细的讲解面向对象的各个方面。

对象类的构造


“张全蛋,你去水果公司,把他们的组装零件需求清单带过来~,并且还要带上组装的技术说明书。”车间主任吆喝着叫张全蛋办事。张全蛋前往了水果公司,如愿以偿的拿到了他想要的东西,组装零件清单上写着:

  • amoled屏幕*1
  • 电池3000MA *1
  • CPU*1
  • 内存*1

技术说明上写着:

  • 组装零件:屏幕放置在顶部,电池在底部,中间夹着PCB板,PCB上面封住CPU和内存
  • 开机方法:长按开机键三秒

限于篇幅,我们只列举这些,你可以发现,我们的组装清单上面,事实上就是我们手机的组成部分,需要占用手机内部空间,并且是这个手机的重要组成参数。这就和我们类中的属性和字段的功能是一样的;而技术说明,是对于这里的具体操作,他们是一个工序,一个操作,并不是一个实体,因此他们就是和我们类中的函数是一个意思。

突然一位老员工对张全蛋说,其实啊,每一款水果手机都几乎没多大差别,你可以在机器中预设好内存大小和CPU的型号,这样你就可以直接将模具做好了。面对这种情况,张全蛋想出了一个绝妙的方法,那就是在构造函数中传入参数。

因此我们可以构造出这样一个类

class FruitPhone
{
    public FruitPhone(int msize,string cpuType)
    {
        CpuType = cpuType;
        MemSize = msize;
    }

    public string ScreenType{get;set}
    public string CpuType{get;set;} 
    public int MemSize{get;set;}
    public int Battery{get;set;}

    void Make()
    {
        //todo
    }
    void Open()
    {
        //todo
    }
}

对象的出生


对象就像个体的人,生而入世,死而离世。我们的故事就从对象之生开始吧。首先,看看在上面的例子中,一个对象是如何出生的。

FruitPhone p = new FruitPhone(2,"A12");

我们通过调用构造函数,成功的创造了一个手机对象,在手机被创建的同时,虽然我们还没有组装好屏幕一类的,但是我们在手机模具中也需要预留他们的空间,因此在对象实例化的时候,其内部的每个字段都会被初始化。

对于屏幕和电池一类的,我们后续可能会根据成本等等进行调整,对象的出生也只是完成了对必要字段的初始化操作,其他数据要通过后面的操作来完成。例如对属性赋值,通过方法获取必要的信息等。

对象在内存中的创建过程


关于内存的分配,首先应该了解分配在哪里的问题。CLR 管理内存的区域,主要有三块,分别为:

  • 线程的堆栈,用于分配值类型实例。堆栈主要由操作系统管理,而不受垃圾收集器的控制,当值类型实例所在方法结束时,其存储单位自动释放。栈的执行效率高,但存储容量有限。
  • GC 堆,用于分配小对象实例。如果引用类型对象的实例大小小于 85000 字节,实例将被分配在 GC 堆上,当有内存分配或者回收时,垃圾收集器可能会对 GC 堆进行压缩,详情见后文讲述。
  • LOH(Large Object Heap)堆,用于分配大对象实例。如果引用类型对象的实例大小不小于 85000 字节时,该实例将被分配到 LOH 堆上,而 LOH 堆不会被压缩,而且只在完全 GC 回收时被回收。

对于分配在堆栈上的局部变量来说,操作系统维护着一个堆栈指针来指向下一个自由空间的地址,并且堆栈的内存地址是由高位到低位向下填充。

而对于引用类型的实例分配于托管堆上,而线程栈却是对象生命周期开始的地方。对 32 位处理器来说,应用程序完成进程初始化后,CLR 将在进程的可用地址空间上分配一块保留的地址空间,它是进程(每个进程可使用 4GB)中可用地址空间上的一块内存区域,但并不对应于任何物理内存,这块地址空间即是托管堆。托管堆又根据存储信息的不同划分为多个区域,其中最重要的是垃圾回收堆(GC Heap)和加载堆(Loader Heap),GC Heap 用于存储对象实例,受 GC 管理;Loader Heap 又分为 High-Frequency Heap、Low-Frequency Heap 和 Stub Heap,不同的堆上又存储不同的信息。Loader Heap 最重要的信息就是元数据相关的信息,也就是 Type 对象,每个 Type 在 Loader Heap 上体现为一个 Method Table(方法表),而 Method Table 中则记录了存储的元数据信息,例如基类型、静态字段、实现的接口、所有的方法等等。Loader Heap 不受 GC 控制,其生命周期为从创建到 AppDomain 卸载。

对于本例中的对象创建,首先会在栈中声明一个指向堆中数据的指针(引用),它占用4个字节,然后调用newobj指令,搜索该类是否含有父类,如果有,则从父类开始分配内存,对于本例中,FruitPhone对象所需要的内存为4字节的string引用两个,4字节的int*2。实例对象所占的字节总数还要加上对象附加成员所需的字节总数,其中附加成员包括 TypeHandle 和 SyncBlockIndex,共计 8 字节(在 32 位 CPU 平台下),共计24字节。

CLR 在当前 AppDomain 对应的托管堆上搜索,找到一个未使用的 20 字节的连续空间,并为其分配该内存地址。事实上,GC 使用了非常高效的算法来满足该请求,NextObjPtr 指针只需要向前推进 20 个字节,并清零原 NextObjPtr 指针和当前 NextObjPtr 指针之间的字节,
然后返回原 NextObjPtr 指针地址即可,该地址正是新创建对象的托管堆地址,也就是p引用指向的实例地址。而此时的 NextObjPtr 仍指向下一个新建对象的位置。注意,栈的分配是向
低地址扩展,而堆的分配是向高地址扩展。

最后,调用对象构造器,进行对象初始化操作,完成创建过程。该构造过程,又可细分为
以下几个环节:

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

上述过程,基本完成了一个引用类型创建、内存分配和初始化的整个流程,然而该过程只能看作是一个简化的描述,实际的执行过程更加复杂,涉及到一系列细化的过程和操作。

补充


静态字段的内存分配和释放,又有何不同?

静态字段也保存在方法表中,位于方法表的槽数组后,其生命周期为从创建到 AppDomain
卸载。因此一个类型无论创建多少个对象,其静态字段在内存中也只有一份。静态字段只能由静
态构造函数进行初始化,静态构造函数确保在类型任何对象创建前,或者在任何静态字段或方法
被引用前执行,其详细的执行顺序请参考相关讨论。

对象的消亡


对象的生命周期由 GC 控制,其规则大概是这样:GC 管理所有的托管堆对象,当内存回收执行时,GC 检查托管堆中不再被使用的对象,并执行内存回收操作。不被应用程序使用的对象,指的是对象没有任何引用。关于如何回收、回收的时刻,以及遍历可回收对象的算法,是较为复杂的问题,我们将在 后续进行深度探讨。

技术图片

.NET Core 类的生命周期

标签:内存区域   创建对象   控制   数据信息   info   方法   相关   inf   com   

原文地址:https://www.cnblogs.com/cool2feel/p/13439548.html


评论


亲,登录后才可以留言!