有過C,C++開發經歷的同學,肯定對當時碰到的內存訪問越界或者內存泄漏深惡痛絕,哪怕后續有了智能指針這些東西,還是不能完全避免此類問題。
而C#和Java擁有的自動內存管理機制,讓程序員可以不必自己去管理內存,專注於功能開發。
所謂內存管理,必然是運行時的事情, 而C#和Java之所以可以做到自動管理,就是因為它們在真正的機器二進制OS上有了自己的運行時(虛擬機)。
所以首先可以看一下他們各自的運行時, C#的CLR 和 Java的JVM
CLR vs JVM
下面的流程圖,基本描述了C#和Java總體的一個從源代碼到最終的可執行的過程。
源代碼通過各自的編譯器,編譯成中間件的編碼,這些中間件的編碼可以被各自的運行時認識並管理,運行時載入中間件編碼,轉換成機器二進制運行在操作系統上。

運行時的內存區域
要明白內存管理,首先必須知道運行時各種內存分配情況。
其實總體來說,CLR和JVM中的內存分類基本一致,只是各自的邏輯划分和稱謂有些差異。
比較大的兩塊分別是堆和棧。
棧都是線程私有的,每個方法在執行時都會創建一個棧幀用於存儲局部變量、操作數棧、方法出口等等信息。棧的生命周期與線程相同,而棧中存放的內存,在各自屬於的方法執行完成后,就會自動釋放。
堆是所有線程共享的。幾乎所有的引用對象實例都是存放在這個區域的。而CLR或者JVM中下了大力氣來管理的就是堆這塊的內存區域,這種管理機制一般被稱之為垃圾回收。
一般來說,還有方法區(用於存儲已經被加載的類信息,即時編譯后的代碼等數據,CLR中一般稱做Type Object 區,JVM中叫Method Area),運行時常量池(存放各種編譯時生成的字面量符號引用等信息),這些都可以認為是堆中的邏輯分塊。
垃圾回收機制
垃圾回收機制就是用來管理堆中的內存了,當堆中的某個對象不再被使用到的時候,可以自動的將這塊內存回收,置成可用狀態。
可達性分析
那么如何知道某個對象不再被用到了呢,這個一般都是基於可達性分析后生成可達對象圖來實現的;可達性分析就是通過從一些根對象出發,去遍歷每一個被引用到的對象,當一個對象到根對象沒有任何引用路徑的時候,就認為該對象可以被回收。
根對象一般包括:棧中定義的局部變量,全局的靜態對象等等。
而這里說的引用關系,其實是指強引用。而和強引用相對的,還有弱引用。
強弱引用
弱引用的引入其實是和垃圾回收器緊密相關的,因為垃圾回收的促發點是難以掌控的,而有一些對象的創建確實需要很大開銷的,並且這些對象也不需要非常頻繁的訪問和很長的生命周期;那么作為平衡,就有了這個弱引用機制。弱引用即表示引用的對象可以被垃圾回收正常回收,但是如果在垃圾回收回收它之前想用它,還可以通過弱引用重新拿回來這個對象進行使用。
CLR中只有強引用/弱引用之分。
而JVM中除了這兩個之外,額外還有軟引用和虛引用兩種:
軟引用是用來描述一些還有用但是非必須的對象,被軟引用關聯着的對象,只有在發生outofmemory的情況下才會被回收。
而虛引用不對對象的生命周期和可達性分析產生任何影響,它唯一的作用是提供一種機制,可以在該對象被回收的時候收到一個系統通知。
回收算法
上面提到,通過可達性分析可以標記出所有需要回收的對象。那么如何回收呢?
最簡單的當然就是清除掉需要回收的對象的內存空間即可,這種方法實現簡單,而且因為不改變存活的對象的地址,可以大大減少GC時的延時。但是帶來的問題也很嚴重,它會讓內存碎片化,最后導致雖然還有很多可用的內存,但是因為沒有連續的可用的大內存而outofmemory。
另外一種方法,就是需要對內存進行壓縮處理,這種方法可以大大提高內存的利用率,並且為后續的內存分配帶來便利(之后從已用空間的后面追加分配即可)。
對於內存的壓縮處理,其實也可以分為兩種處理手段,復制和整理。
復制算法就是把還存活的對象復制到另外一塊內存中,它不太適合垃圾回收時有大量存活對象的情況,因為在這種情況下會需要准備大量的可用內存空間供復制使用。但是對於很少有存活對象的情況下,此種算法非常高效。
整理就是在清楚了可回收對象后,將存活對象的位置移動,使得他們的內存成為連續的一整塊,這種算法在很少可回收對象的情況下使用比較廣泛。
分代
我們上面提到了各種不同的回收算法,各自適用與不同的場景。而CLR和JVM中分別通過對對象存活周期的分組來適配這些場景,然后每種場景去適用最高效的回收算法,這種分組稱為代。
CLR中有三個代,分別是第0代,第1代,和第2代。每次促發垃圾回收后仍然存活的對象自動上升到下一個代。
JVM中有兩個代,分別時新生代和老年代,新生代中一般還有兩個區域eden區域和Survior區域,對象首次分配時都在eden區域,每次垃圾回收時候,如果一個對象還存活,會被復制到Survior區域,並且年齡+1,年齡到一定程度后就會進入老年代,如果新生代中的Survior區域的內存在GC時不夠用了,那么這些存活的對象會直接進入老年代。
還有一個大對象堆一說,其實大對象堆在CLR中可以被認為也在第2代中,JVM中大對象在老年代中。
一般來說新生代或者第0代,都是采取的標記-復制的算法,而第1,2代或者老年代采用標記-整理的方法比較常見,當然大對象一般采用標記-清除的方式
