.NET Core CSharp初級篇1-7 類的生命歷程

.NET Core CSharp初級篇 1-7

本節內容爲類的生命週期git

引言

對象到底是一個什麼東西?對於許多初學者而言,對象都是一個很是抽象的知識點。若是非要用一句話描述,我以爲「萬物皆對象」是對於對象最全面的概述了。本節內容中,咱們將以在富土康打工的張全蛋組裝一臺水果手機做爲例子,詳細的講解面向對象的各個方面。github

對象類的構造

「張全蛋,你去水果公司,把他們的組裝零件需求清單帶過來~,而且還要帶上組裝的技術說明書。」車間主任吆喝着叫張全蛋辦事。張全蛋前往了水果公司,如願以償的拿到了他想要的東西,組裝零件清單上寫着:算法

  • amoled屏幕*1
  • 電池3000MA *1
  • CPU*1
  • 內存*1

技術說明上寫着:數組

  • 組裝零件:屏幕放置在頂部,電池在底部,中間夾着PCB板,PCB上面封住CPU和內存
  • 開機方法:長按開機鍵三秒

限於篇幅,咱們只列舉這些,你能夠發現,咱們的組裝清單上面,事實上就是咱們手機的組成部分,須要佔用手機內部空間,而且是這個手機的重要組成參數。這就和咱們類中的屬性和字段的功能是同樣的;而技術說明,是對於這裏的具體操做,他們是一個工序,一個操做,並非一個實體,所以他們就是和咱們類中的函數是一個意思。多線程

忽然一位老員工對張全蛋說,其實啊,每一款水果手機都幾乎沒多大差異,你能夠在機器中預設好內存大小和CPU的型號,這樣你就能夠直接將模具作好了。面對這種狀況,張全蛋想出了一個絕妙的方法,那就是在構造函數中傳入參數。ide

所以咱們能夠構造出這樣一個類函數

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
    }
}

對象的出生

對象就像個體的人,生而入世,死而離世。咱們的故事就從對象之生開始吧。首先,看看在上面的例子中,一個對象是如何出生的。ui

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

咱們經過調用構造函數,成功的創造了一個手機對象,在手機被建立的同時,雖然咱們尚未組裝好屏幕一類的,可是咱們在手機模具中也須要預留他們的空間,所以在對象實例化的時候,其內部的每一個字段都會被初始化。spa

對於屏幕和電池一類的,咱們後續可能會根據成本等等進行調整,對
象的出生也只是完成了對必要字段的初始化操做,其餘數據要經過後面的操做來完成。例如對屬性賦值,經過方法獲取必要的信息等。操作系統

對象在內存中的建立過程

關於內存的分配,首先應該瞭解分配在哪裏的問題。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

博客園

相關文章
相關標籤/搜索