.NET Core CSharp 初級篇 1 7 本節內容為類的生命周期 引言 對象究竟是一個什麼東西?對於許多初學者而言,對象都是一個非常抽象的知識點。如果非要用一句話描述,我覺得“萬物皆對象”是對於對象最全面的概述了。本節內容中,我們將以在富土康打工的張全蛋組裝一臺水果手機作為例子,詳細的講解面 ...
.NET Core CSharp初級篇 1-7
本節內容為類的生命周期
引言
對象究竟是一個什麼東西?對於許多初學者而言,對象都是一個非常抽象的知識點。如果非要用一句話描述,我覺得“萬物皆對象”是對於對象最全面的概述了。本節內容中,我們將以在富土康打工的張全蛋組裝一臺水果手機作為例子,詳細的講解面向對象的各個方面。
對象類的構造
“張全蛋,你去水果公司,把他們的組裝零件需求清單帶過來~,並且還要帶上組裝的技術說明書。”車間主任吆喝著叫張全蛋辦事。張全蛋前往了水果公司,如願以償的拿到了他想要的東西,組裝零件清單上寫著:
- 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 檢查托管堆中不再被使用的對象,並執行記憶體回收操作。不被應用程式使用的對象,指的是對象沒有任何引用。關於如何回收、回收的時刻,以及遍歷可回收對象的演算法,是較為複雜的問題,我們將在 後續進行深度探討。不過,這個回收的過程,同樣使我們感慨。大自然就是那個看不見的 GC,造物而又終將萬物回收,無法改變。我們所能做到的是,將生命的周期拓寬、延長、書寫得更加精彩
如果我的文章幫到了你,請在博客園下麵點一個推薦,在github項目頁面點一顆星,謝謝
Reference
你必須知道的.NET