GC機制,你真的瞭解嗎?

前言

GC(Garbage Collection)相信是每一個程序猿(媛)都熟知的了。作爲一個Android開發者,無疑我們是幸福的,因爲我們不用像C語言那樣還需要手動進行垃圾回收,但同時我們又是不幸的,由於android市場的碎片化,各個型號的手機迥然不同,適配起來相當麻煩,其中最需要避免的就是Out Of Memory了。那麼深入的瞭解GC機制就是每一個android開發者的必修課。

 

JVM運行時的內存分配機制

內存分配圖

JVM的內存分配可以通過上圖來說明,程序運行時會將所申請的內存分爲五大塊,而不僅僅是我們通常所說的堆內存和棧內存。它們分別是一個程序計數器、一個方法區、一個堆內存和兩個棧內存(虛擬機棧和本地方法棧)。

程序計數器:可能比較陌生,但它的概念和用途卻是很好理解的,程序計數器是線程私有的,生命週期隨線程的創建而創建,隨線程消亡而消亡。主要用於記錄當前線程執行的位置。通常我們所用的線程切換、循環、跳轉等都需要依賴這個計數器來完成。

方法區:主要用於存儲已經被JVM加載的常量、靜態變量等,它是能夠被線程共享的。

本地方法棧:主要針對的是native方法,因此一般在JNI開發、NDK開發涉及較多。它是線程私有的。

虛擬機棧:也是線程私有的,JVM是基於棧的解釋器執行的。每個方法被執行的時候,JVM都會在虛擬機中創建一個棧幀(Stack Frame)。而每一個棧幀內又包含了局部變量表、操作數棧、動態鏈接、返回地址等。

堆(Heap):又稱爲GC堆,是JVM所分配的最大的一塊內存區塊,是線程共享的,它也是垃圾回收機制回收的主要區塊,因爲它內存存放的是對象的實例,幾乎所有的對象的實例都存放在堆內存中。下面我們所介紹的GC相關內容基本是堆內存中發生的。

 

什麼是GC機制?

要理解什麼是垃圾回收機制,首先我們得知道什麼是“垃圾”。在java虛擬機中認爲內存中已經沒有用的對象即是“垃圾”,需要被及時回收。

那麼虛擬機又是如何去判斷出內存中的對象是不是“垃圾”的呢,虛擬機是通過一種叫做“可達性分析算法”來定位內存“垃圾”的。可達性分析算法是從離散數學中引入的,它認爲在可以將一組“GC Root”對象作爲起始點,然後從這些起始點向下搜索形成一條引用鏈,所有存在於引用鏈中的對象即爲有用的對象,反之,沒有被引用鏈所涵蓋的對象則視爲“垃圾”對象,需要被系統回收。

可達性分析算法圖解

上圖是我畫的簡略的可達性分析算法的圖解,圖中綠色表示可達對象,灰色表示“垃圾”對象。紅色表示引用鏈。在上圖所示中存在兩條完整的引用鏈,分別是GC Root -> A -> D -> E 和 GC Root -> C。在內存不足或手動觸發GC回收時,B、F、G則會視爲“垃圾”被系統回收掉。

在JAVA中GC Root對象包括下面幾種:

(1). 虛擬機棧(棧幀中的局部變量區,也叫做局部變量表)中引用的對象。

(2). 方法區中的類靜態屬性引用的對象。

(3). 方法區中常量引用的對象。

(4). 本地方法棧中JNI(Native方法)引用的對象。

那麼未被引用鏈所連接的對象在GC時就一定會被殺死嗎?其實不然,如果要對一個對象進行死亡宣判,至少需要經歷兩次標記過程。如果對象在進行可達性分析後發現沒有與GCRoots相連的引用鏈,則該對象被第一次標記並進行一次篩選,篩選條件爲是否有必要執行該對象的finalize方法,若對象沒有覆蓋finalize方法或者該finalize方法是否已經被虛擬機執行過了,則均視作不必要執行該對象的finalize方法,即該對象將會被回收。反之,若對象覆蓋了finalize方法並且該finalize方法並沒有被執行過,那麼,這個對象會被放置在一個叫F-Queue的隊列中,之後會由虛擬機自動建立的、優先級低的Finalizer線程去執行,對F-Queue中對象進行第二次標記,如果對象在finalize方法中拯救了自己,即關聯上了GCRoots引用鏈,如把this關鍵字賦值給其他變量,那麼在第二次標記的時候該對象將從“即將回收”的集合中移除,如果對象還是沒有拯救自己,那就會被回收。

接下來我們思考一個問題,垃圾回收是否會影響程序的性能?答案是肯定的。GC本質上來講還是運行了一段代碼來清除無用的對象,只要是運行代碼就一定有耗時,一旦頻繁的進行耗時操作,則會影響程序的性能,更嚴重的可能會直觀的體現在卡頓上。GC是在堆內存不足導致內存分配失敗或者開發者手動調用System.gc()時被觸發。而一般情況下,開發者們是不會手動去觸發GC的,所以合理的堆內存分配至關重要。

 

分代回收策略

分代回收策略是java虛擬機根據對象存活的週期不同,講堆內存分爲新生代、老年代,有些虛擬機中還存在永久代。然後在觸發GC時進行分代回收,優先回收新生代中的“垃圾”對象。

在新生代中,又按照8:1:1比例被細分爲Eden、Survivor0、Survivor1。

分代回收策略示意圖

一個對象被創建時,它首先會進入新生代的Eden分區中,當第一次GC時,若該對象不可達,則被回收,否則則被移入到Survivor0中,之後每次GC,都會檢測可達性,若可達,就會在Survivor0和Survivor1之間來回存放。一般情況下當在新生代中存活超過15次GC,該對象則會被移入到老年代。

老年代的內存大小一般比新生代大,能存放更多的對象,有時候當對象所需內存過大,而新生代內存又不足時,該對象會被直接分配到老年代中。在老年代中還維護了一個512byte的card table。用來記錄所有老年代對象引用新生代的對象的信息。一旦新生代發生GC時,只需要檢查老年代中的card table就能判斷出老年代對象所引用的可達性,大大提高了性能。

 

實現垃圾回收機制的4種算法

標記清除算法(Mark and Sweep GC)

標記清除算法很好理解,首先對每一個對象維護一個標記,然後通過可達性分析算法生成引用鏈,不在引用鏈上的對象則被標記,最後就直接清除這些被標記的對象即可。這種算法不需要移動對象,簡單方便,但卻可能會產生內存碎片,提高GC的頻率。

複製算法(Copying)

複製算法則是將內存空間分爲兩個可用區塊,每次只使用其中一塊,當需要GC時,首先判斷可達性並標記,然後將所有可達對象都複製到另一塊內存中,最後直接刪除當前這塊內存中的所有對象即可。這種方式實現簡單、運行高效,也不用考慮內存碎片。但是由於需要平分內存爲兩塊,大大減小了內存的利用率。

標記壓縮算法(Mark-Compact)

標記壓縮算法算是第一種算法的優化,它是在標記清除算法的基礎上最後將所有的存活對象壓縮到內存的某一端。這種方式既避免了內存碎片,又不用減小內存利用率,性價比很高,但這種壓縮方式還是需要移動對象,因此在一定程度上還是降低了效率。

分代回收算法

分代回收算法就是利用上面我們所說的分代回收策略和其他回收算法所結合而產生的,比如新生代中的Survivor區塊就是採用的複製算法,而在老年代中則是採用的標記壓縮算法。

 

四種引用方式

有時候我們不希望某一些對象被系統回收,這時我們需要通過引用方式來進行控制。Java中存在四種引用方式,由強到若分別是強引用、軟引用、弱引用和虛引用。

強引用(StrongReference)

1.只要某個對象有強引用與之關聯,JVM必定不會回收這個對象。

2.即使內存不足,JVM寧願拋出OutOfMemory錯誤也不會回收這種對象。

軟引用(SoftReference)

1.用來描述一些有用但並不是必須的對象。

2.對於軟引用關聯着的對象,只有在內存不足的時候JVM纔會回收該對象。

弱引用(WeakReference)

1.弱引用是用來描述非必須的對象。

2.當JVM進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。

虛引用(PhantomReference)

1.不影響對象的生命週期。

2.如果一個對象與虛引用關聯,則跟沒有引用與之關聯一樣。

3.在任何時候都可能被垃圾回收器回收。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章