44

.NET Core CSharp初级篇1-7 类的生命历程 - WarrenRyan

 4 years ago
source link: https://www.cnblogs.com/WarrenRyan/p/11256571.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

.NET Core CSharp初级篇 1-7

本节内容为类的生命周期

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

对象类的构造

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

  • amoled屏幕*1
  • 电池3000MA *1
  • CPU*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 检查托管堆中不再被使用的对象,并执行内存回收操作。不被应用程序使用的对象,指的是对象没有任何引用。关于如何回收、回收的时刻,以及遍历可回收对象的算法,是较为复杂的问题,我们将在 后续进行深度探讨。不过,这个回收的过程,同样使我们感慨。大自然就是那个看不见的 GC,造物而又终将万物回收,无法改变。我们所能做到的是,将生命的周期拓宽、延长、书写得更加精彩

如果我的文章帮到了你,请在博客园下面点一个推荐,在github项目页面点一颗星,谢谢

Reference

你必须知道的.NET

Github

BiliBili主页

WarrenRyan's Blog

博客园


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK