GC是什麼?為什麼我們要去使用它

天天不是小可愛發表於2019-08-05

GC(Garbage Collection)是各大語言的寵兒,也是電腦科學領域裡很熱門的一個話題。最早在JVM中有看過這個演算法,後來發現即使是js這種指令碼語言也是有GC的。單純就JVM來說的話,GC演算法也在不斷地改進,成熟。從最早的序列到高頓吞吐量的並行,為了解決高延遲又演化出了CMS(Concurrent Mark Sweep),為了解決碎片的問題,又開發了G1.

為什麼我們需要進行GC呢


在早期的程式語言中,程式執行中沒有棧幀(Stack Frame)去維護,所以採用的是靜態分配策略,這比動態分配要快不少,但是它有一個很不人性化的缺點就是需要在編譯期的時候確定程式所需要的資料結構大小。

在1958年,Algol-58 語言首次提出了塊結構(block-structured),塊結構語言通過在記憶體中申請棧幀來實現按需分配的動態策略。在過程被呼叫時,幀(frame)會被壓到棧的最上面,呼叫結束時彈出。棧分配策略賦予程式設計師極大的自由度,區域性變數在不同的呼叫過程中具有不同的值,這為遞迴提供了基礎。但是後進先出(Last-In-First-Out, LIFO)的棧限制了棧幀的生命週期不能超過其呼叫者,而且由於每個棧幀是固定大小,所以一個過程的返回值也必須在編譯期確定。所以誕生了新的記憶體管理策略——堆(heap)管理。

現在計算機VS圖靈機


現代計算機相對於圖靈機來說,本質上的區別就在於資源有限性,所以在使用完各種資源以後,需要將其釋放(release)。

那釋放資源全都用GC可以嘛


GC本來就是給馬大哈的程式設計師準備的,人為的手動釋放資源很容易出問題,那我能不能在每次需要釋放資源的時候都呼叫GC演算法呢?這樣不是一勞永逸嘛。

並不能,GC同樣有它適用的和不適用的場景,在(socket/file handle)的使用中沒有使用GC,很大一部分原因在於它的不確定性。你即使呼叫了GC你也不知道它啥時會回收,它到底會不會回收,這樣的GC還是不可靠(霧,GC都是大豬蹄子

但是如果我們顯示對資源進行回收就不一樣了,呼叫了close/destroy後,資源百分百就被釋放掉了

為啥在記憶體裡面就可以用GC呢,有兩個原因,第一是它具有獨佔性(當然你也可以叫唯一性)OS給每個執行的程式分配的記憶體都是唯一的,也是相互獨立的,咋倆互不干擾,我晚一點回收對你也沒有什麼影響。第二點是記憶體夠用,日常使用程式中也沒見記憶體佔用太高,我晚一點回收也不會OutOfMenmory,那我就放心大膽地用GC的回收機制。

由這兩點可見,對於資源比較緊張的一些嵌入式的裝置,還是手動釋放資源來的較好。(不包括樹莓派)

GC是什麼


GC的本質是記憶體的自動管理,用來回收堆(Heap)中不再需要(使用)的物件。

我們知道記憶體其實也會被劃分為各個區域的,常見的stack和heap。stack裡面裝的是區域性變數,函式的呼叫結束以後也就回收了,這個是個固定的流程,所以不需要GC。Heap中的空間,用來在多個函式之間去共享資料,由程式自己來動態申請,這個時候我們就需要利用到GC了

GC由兩大核心組成

  • 物件識別,其實也就是常說的判斷物件是否存活,live object和garbage
  • 回收演算法,什麼時候回收,如何回收

那咋辦嘛


先從物件識別開始說下吧,判斷物件是live object還是garbage

在堆(Heap)裡存放著Java中基本上所有物件的例項,當一個物件沒有引用且不會再引用的時候就可以認為他已經死去,視為garbage

  1. 引用計數(Python和PHP都採用這種方式)

給物件一個引用計數器(在物件頭加上一個counter),當引用的時候計數器就加一,引用失效就減一,引用次數為0的時候,該物件就失效了。

這個很像OS裡面的PV原語,實現是很簡單的,效率也比較高。但缺點也是顯而易見的,就是很難去解決物件之間迴圈使用的問題。

舉個栗子 如果現在有兩個物件 OA OB ,OA.instance = OB;OB.instance=OA;它們兩個相互引用,導致counter都不為0,於是都不會被回收

  1. 可達性分析(Tracing)

這個是現代語言使用最廣泛的一種方式,從root物件開始,不斷的去追蹤(tracing),找到所有可以被引用的物件,那這些物件就可以被稱作是可達的(reachable),剩下的物件自然就是不可達物件,視作garbage將其回收。

那麼,你說的這個root物件,它是什麼樣子的呢?
  • Java 虛擬機器棧(棧幀中的本地變數表)中引用的物件
  • 本地方法棧中引用的物件
  • 方法區中常量引用的物件
  • 方法區中類靜態屬性引用的物件

被堆中物件所引用的物件,它們就不算Roots了,這樣就不會引起迴圈引用的問題。

引用


判定物件是否存活與“引用”有關。在 JDK 1.2 以前,Java 中的引用定義很傳統,一個物件只有被引用或者沒有被引用兩種狀態,我們希望能描述這一類物件:當記憶體空間還足夠時,則保留在記憶體中;如果記憶體空間在進行垃圾手收集後還是非常緊張,則可以拋棄這些物件。很多系統的快取功能都符合這樣的應用場景。

在 JDK 1.2 之後,Java 對引用的概念進行了擴充,將引用分為了以下四種。不同的引用型別,主要體現的是物件不同的可達性狀態reachable和垃圾收集的影響。

強引用(Strong Reference)

類似 "Object obj = new Object()" 這類的引用,就是強引用,只要強引用存在,垃圾收集器永遠不會回收被引用的物件。但是,如果我們錯誤地保持了強引用,比如:賦值給了 static 變數,那麼物件在很長一段時間內不會被回收,會產生記憶體洩漏。

軟引用(Soft Reference)

軟引用是一種相對強引用弱化一些的引用,可以讓物件豁免一些垃圾收集,只有當 JVM 認為記憶體不足時,才會去試圖回收軟引用指向的物件。JVM 會確保在丟擲 OutOfMemoryError 之前,清理軟引用指向的物件。軟引用通常用來實現記憶體敏感的快取,如果還有空閒記憶體,就可以暫時保留快取,當記憶體不足時清理掉,這樣就保證了使用快取的同時,不會耗盡記憶體。

弱引用(Weak Reference)

弱引用的強度比軟引用更弱一些。當 JVM 進行垃圾回收時,無論記憶體是否充足,都會回收只被弱引用關聯的物件。

虛引用(Phantom Reference)

虛引用也稱幽靈引用或者幻影引用,它是最弱的一種引用關係。一個物件是否有虛引用的存在,完全不會對其生存時間構成影響。它僅僅是提供了一種確保物件被 finalize 以後,做某些事情的機制,比如,通常用來做所謂的 Post-Mortem 清理機制。

事實上,軟引用,弱引用和虛引用都可以被叫做弱引用

以弱引用的方式指向一個物件,弱引用不會保護該物件被 GC 回收。如果該物件被回收了,那麼這個弱引用會被賦予一個安全值(一般為NULL)。

可以看一下這個wiki,講了一下弱引用如何解決迴圈引用Dealing with reference cycles

參考


相關文章