1. 程式人生 > >Unity GC優化學習(一):認識堆(heap)&棧(stack)

Unity GC優化學習(一):認識堆(heap)&棧(stack)

儘管在.NET framework 下我們並不需要擔心記憶體管理和垃圾回收(GarbageCollection),但是我們還是應該瞭解它們,以優化我們的應用程式。
同時還需要具備一些基礎的記憶體管理工作機制的知識,這樣有助於解釋日常程式編寫中的變數的行為。
本文將學習和理解堆和棧的基本知識,變數型別以及為什麼一些變數能夠按照它們自己的方式工作。

在.NET framework環境下,當我們執行程式碼時,記憶體中有兩個地方用來儲存這些程式碼:堆和棧。

·Question1:堆和棧有什麼不同?

    【申請速度】
    ·堆的申請速度較慢,容易產生記憶體碎片,但是用起來比較方便。
    ·而棧的申請速度較快,但卻不受程式設計師的控制。
    【申請大小】
    ·棧申請的容量較小1-2M,堆申請大小雖受限於系統中有效的虛擬記憶體,但較大
    【儲存內容】
    ·棧負責儲存我們程式碼執行或呼叫的路徑,而堆則負責儲存物件或者說資料的路徑。

可以將棧想象成一堆從頂向下堆疊的盒子。每當呼叫一次方法是,我們應將應用程式中所要發生的事情記錄在棧頂的一個盒子中。而我們每次只能夠使用棧頂的那個盒子。當我們棧頂的盒子被使用完之後,或者方法執行完畢之後,我們將拋開這個盒子然後繼續使用棧頂上的新盒子。
堆的工作原理比較相似,但大多數時候堆被用作儲存資訊而非儲存執行路徑,因此能夠在任意時間被訪問。
與棧相比堆沒有任何訪問限制,堆就像一個倉庫,儲存著我們使用的各種物件等資訊,而棧就像儲物櫃裡堆疊的鞋盒,我們只能從最頂層的盒子開始取, 直到發現那隻最適合的。
和棧不同的是,堆中儲存的資訊在被呼叫完畢不會立即被清理掉。

這裡寫圖片描述

棧是自行維護的,也就是說記憶體自動維護棧,當棧頂的內容不再被使用,該內容將會被跑出。相反,堆需要考慮垃圾回收。

·Question2:堆和棧裡有些什麼?

當我們的程式碼執行的時候,堆和棧中主要放置了四種類型的資料:

    值型別(Value Type)
    引用型別(Reference Type)
    指標(Pointer)
    指令(Instruction)

1.值型別
在C#中,所有被宣告為一下型別的事物被稱為值型別:

bool
byte
char
decimal
double
enum
float
int
long
sbyte
struct
unit
ulong
ushort

2.引用型別:

所有被宣告為一下型別的事物被稱為引用型別

class
interface
delegate
object
string
StringBuilder

3.指標

在記憶體管理方案中放置的第三種類型就是型別引用,引用通常就是一個指標。指標或引用是不同於引用型別的,這是因為當我們說某個事物是一個引用型別時就意味著我們是通過指標來訪問它的。指標是一塊記憶體空間,而它指向另一個記憶體空間。

這裡寫圖片描述

大白話:

    對於引用型別而言,其記憶體地址(指標)存放在棧上
    其實際內容則由棧上的地址索引儲存在堆上

4.指令

  • 如何決定放哪?
    這裡有一條黃金規則:
    1.引用型別總是放在堆中。
    2.值型別和指標總是放在他們被宣告的地方。

就像前面提到的那樣,棧是負責儲存我們的程式碼執行或呼叫時的路徑。當我們的程式碼開始呼叫一個方法時,將放置一段編碼指令(在方法中)到棧上,緊接著放置方法的引數,然後程式碼執行到方法中被“壓棧”至棧頂的變數位置。通過以下的例子很容易理解:

下面是一個方法:

public int PlusSix(int paramValue)
{
    int result;
    result = paramValue + 6;
    return result;
}

現在就來看看在棧頂發生了些神馬。

首先方法入棧(只包含需要執行的邏輯位元組,即執行該方法的指令,而非方法體內的資料),緊接著是方法的引數入棧。

接著,控制(即執行方法的執行緒)被傳遞到堆疊中PlusSix()的指令上,

當方法執行時,我們需要在棧上為“result”變數分配一些記憶體,

方法執行完成,然後方法的結果被返回。

通過將指標指向PlusSix()方法曾使用的可用的記憶體地址,所有棧上的該該方法所使用的記憶體都被清空,且程式將自動回到棧上最初的方法呼叫的位置(本例中不會看到)。


在這個例子中,我們的“result”變數是被放置在棧上的,事實上,當值型別資料在方法體重被宣告時,它們都是被放置在棧上的。
值型別資料有時也被放置在堆上。
記住這條規則——值型別總是放在它們被宣告的地方。好滴,如果一個值型別資料在方法體外被宣告,而且存在於一個引用型別中,那麼他將被堆中的引用型別所取代。

來看另外一個例子:

假如我們有這樣一個MyInt類:

public class MyInt
{
    public int MyValue;
}

然後執行下面的方法:

public MyInt PlusSix(int praramValue)
{
    MyInt result = new MyInt();
    result.MyValue = praramValue + 6;
    return result;
}

就像前面提到的,方法以及方法的引數被放置到棧上,接下來,控制被傳遞到堆疊中的PlusSix()的指令上。

接著會出現一些有趣的現象……

因為“MyInt”是一個引用型別,它將被放置在堆上,同時在棧上生成一個指向這個堆的指標引用。

在PlusSix()方法執行之後,我們將清空剛剛使用的棧頂部分。

我們將剩下孤獨的MyInt物件在堆中(棧中將不會存在任何指向 MyInt物件 的指標!)

這就是垃圾回收器(後簡稱GC)起作用的地方。
當我們的程式達到了一個特定的記憶體閾值,我們需要更多的堆空間的時候,GC開始起作用。
GC將停止所有正在執行的執行緒,找出堆中存在的所有不再被主程式訪問的物件,並刪除它們。
然後GC會重新組織堆中所有剩下的物件來節省空間,並調整棧和堆中所有與這些物件相關的指標。
你肯定會想到這個過程非常耗費效能,所以這是你就會知道為什麼我們需要如此重視棧和堆裡有些什麼,特別是在需要編寫提高效能的程式碼是。

太棒了,但它是如何影響我的?

GoodQuestion!

當我們使用引用型別時,我們實際上是在處理該型別的指標,而非該型別本身。當我們使用值型別是,我們是在使用值型別本身。