天天看點

GC垃圾回收機制GC垃圾回收機制

GC垃圾回收機制

GC概述

垃圾回收是一種自動的存儲管理機制。 當一些被占用的記憶體不再需要時,就應該予以釋放,以讓出空間,這種存儲資源管理,稱為垃圾回收(Garbage Collection)。

1.為什麼要有GC?

GC是垃圾收集的意思(Gabage Collection),記憶體處理是程式設計人員容易出現問題的地方,忘記或者錯誤的記憶體回收會導緻程式或系統的不穩定甚至崩潰,Java提供的GC功能可以自動監測對象是否超過作用域進而達到自動回收記憶體的目的,Java語言沒有提供釋放已配置設定記憶體的顯示操作方法。是以Java 程式員不用擔心記憶體管理,因為垃圾收集器會自動進行管理

2.什麼時候?換句話說就是GC的觸發條件

GC觸發的條件有兩種:

(1)程式調用System.gc時可以觸發;

(2)系統自身來決定GC觸發的時機。

要了解它的觸發條件,就要先了解一下JVM記憶體空間,請看圖檔

GC垃圾回收機制GC垃圾回收機制

程式計數器:線程私有。是一塊較小的記憶體,是目前線程所執行的位元組碼的行号訓示器。是Java虛拟機規範中唯一沒有規定OOM(OutOfMemoryError)的區域。

Java棧:線程私有。生命周期和線程相同。是Java方法執行的記憶體模型。執行每個方法都會建立一個棧幀,用于存儲局部變量和操作數(對象引用)。局部變量所需要的記憶體空間大小在編譯期間完成配置設定。是以棧幀的大小不會改變。存在兩種異常情況:若線程請求深度大于棧的深度,抛StackOverflowError。若棧在動态擴充時無法請求足夠記憶體,抛OOM。

Java堆:所有線程共享。虛拟機啟動時建立。存放對象實力和數組。所占記憶體最大。分為新生代(Young區),老年代(Old區)。新生代分Eden區,Servior區。Servior區又分為From space區和To Space區。Eden區和Servior區的記憶體比為8:1。 當擴充記憶體大于可用記憶體,抛OOM。

(1)新生代:所有新 new 出來的對象都會最先出現在新生代中,當新生代這部分記憶體滿了之後,就會發起一次垃圾收集事件,這種發生在新生代的垃圾收集稱為 Minor collections。 這種收集通常比較快,因為新生代的大部分對象都是需要回收的,那些暫時無法回收的就會被移動到老年代。全局暫停事件(Stop the World):所有小收集(minor garbage collections)都是全局暫停事件,也就是意味着所有的應用線程都需要停止,直到垃圾回收的操作全部完成。

(2)老年代:老年代用來存儲那些存活時間較長的對象。 一般來說,我們會給新生代的對象限定一個存活的時間,當達到這個時間還沒有被收集的時候就會被移動到老年代中。随着時間的推移,老年代也會被填滿,最終導緻老年代也要進行垃圾回收。這個事件叫做大收集(major garbage collection)。大收集也是全局暫停事件。通常大收集比較慢,因為它涉及到所有的存活對象。是以,對于對相應時間要求高的應用,應該将大收集最小化。此外,對于大收集,全局暫停事件的暫停時長會受到用于老年代的垃圾回收器的影響。

方法區:所有線程共享。用于存儲已被虛拟機加載的類資訊、常量、靜态變量等資料。又稱為非堆(Non – Heap)。方法區又稱“永久代”。GC很少在這個區域進行,但不代表不會回收。這個區域回收目标主要是針對常量池的回收和對類型的解除安裝。當記憶體申請大于實際可用記憶體,抛OOM。

(3)永久代:永久代存儲了描述應用程式類和方法的源資料,JVM 運作應用程式的時候需要這些源資料。 永久代由 JVM 在運作時基于應用程式所使用的類産生。 此外,Java SE 類庫的類和方法可能也存儲在這裡。如果 JVM 發現有些類不在被其他類所需要,同時其他類需要更多的空間,這時候這些類可能就會被垃圾回收。

本地方法棧:線程私有。與Java棧類似,但是不是為Java方法(位元組碼)服務,而是為本地非Java方法服務。也會抛StackOverflowError和OOM。

看完上面的定義之後,接下來再看看GC的系統觸發條件,它的觸發條件分為

Minor GC ,Full GC,那麼就逐個解釋一下。

Minor GC觸發條件:當Eden區滿時,觸發Minor GC。

Full GC觸發條件:

(1)調用System.gc時,系統建議執行Full GC,但是不必然執行

(2)老年代空間不足

(3)方法區空間不足

(4)通過Minor GC後進入老年代的平均大小大于老年代的可用記憶體

3.當觸發GC後,對什麼東西進行的GC?

自動垃圾回收機制就是尋找Java堆中的對象,并對對象進行分類判别,尋找出正在使用的對象和已經不會使用的對象,然後把那些不會使用的對象從堆上清除。

那那些會使用和不會使用的對象是什麼呢?換句java語言就是,當一個對象沒有被引用指向的時候就說明它就是不會使用的對象了。那麼問題來了,怎麼判斷它到底有沒有被引用指向呢?那麼就有了這兩種方式:引用計數法、可達性分析,那麼我就逐個解釋一下。

引用計數法

引用計數算法是垃圾收集器中的早期政策。 在這種方法中,堆中的每個對象執行個體都有一個引用計數。當一個對象被建立時,且将該對象執行個體配置設定給一個引用變量,該對象執行個體的引用計數設定為 1。當任何其它變量被指派為這個對象的引用時,對象執行個體的引用計數加 1(a = b,則b引用的對象執行個體的計數器加1),但當一個對象執行個體的某個引用超過了生命周期或者被設定為一個新值時,對象執行個體的引用計數減 1。

特别地,當一個對象執行個體被垃圾收集時,它引用的任何對象執行個體的引用計數器均減 1。 任何引用計數為0的對象執行個體可以被當作垃圾收集。

引用計數收集器可以很快的執行,并且交織在程式運作中,對程式需要不被長時間打斷的實時環境比較有利,但其很難解決對象之間互相循環引用的問題。

GC垃圾回收機制GC垃圾回收機制

可達性分析

可達性分析算法是從離散數學中的圖論引入的,程式把所有的引用關系看作一張圖,通過一系列的名為 “GC Roots” 的對象作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鍊(Reference Chain)。 當一個對象到 GC Roots 沒有任何引用鍊相連(用圖論的話來說就是從 GC Roots 到這個對象不可達)時,則證明此對象是不可用的。

GC垃圾回收機制GC垃圾回收機制

說到GC roots(GC根),在JAVA語言中,可以當做GC roots的對象有以下幾種:

1、虛拟機棧中的引用的對象。

2、方法區中的類靜态屬性引用的對象。

3、方法區中的常量引用的對象。

4、本地方法棧中JNI的引用的對象。

第一和第四種都是指的方法的本地變量表,第二種表達的意思比較清晰,第三種主要指的是聲明為final的常量值。

4.做了怎麼樣的處理?GC怎麼回收這些沒用的對象呢?

這時候就要看看垃圾回收算法。它總共有四種類型:

( 1 )标記-清除算法

a.标記,也就是垃圾收集器會找出那些需要回收的對象所在的記憶體和不需要回收的對象所在的記憶體,并把它們标記出來,簡單的說,也就是先找出垃圾在哪兒?

所有堆中的對象都會被掃描一遍,以此來确定回收的對象,是以這通常會是一個相對比較耗時的過程

GC垃圾回收機制GC垃圾回收機制

b.清除,垃圾收集器會清除掉上一步标記出來的那些需要回收的對象區域。

存在的問題就是碎片問題,标記清除之後會産生大量不連續的記憶體碎片,空間碎片太多可能會導緻以後在程式運作過程中需要配置設定較大對象時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。它的缺點就是記憶體不連

GC垃圾回收機制GC垃圾回收機制

( 2 )複制算法

标記清除算法每次執行都需要對堆中全部對象掃面一遍效率不高,為解決效率問題,複制算法将記憶體按容量劃分為大小相等的兩塊,每次隻是用其中的一塊。 當這一塊使用完了,就将還存活的對象複制到另一塊上面,然後再把已使用過的記憶體空間一次清理掉。 這樣使得每次都對半區進行記憶體回收,記憶體配置設定時也就不用考慮記憶體碎片等複雜情況,隻要移動堆頂指針,按順序配置設定記憶體即可,實作簡單,運作高效。緊接着它的缺點就來了會減少可用記憶體。

GC垃圾回收機制GC垃圾回收機制

( 3 )标記-整理算法

由于簡單的标記清除可能會存在碎片的問題,是以又出現了壓縮清除的方法,也就是先清除需要回收的對象,然後再對記憶體進行壓縮操作,将記憶體分成可用和不可用兩大部分。

GC垃圾回收機制GC垃圾回收機制

( 4 )分代收集算法

分代算法是上面幾個算法的綜合, Java 堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最适當的收集算法。 在新生代中,每次垃圾收集時都發現有大批對象死去,隻有少量存活,那就選用複制算法,隻需要付出少量存活對象的複制成本就可以完成收集。 而老年代中因為對象存活率較高、沒有額外的空間對它進行配置設定擔保,就必須使用“标記-清除”或者“标記-整理”算法來回收。

拓展:

最後再說一說垃圾收集器的分類吧:

串行收集器(serial collector)隻有一條GC線程,在運作是會暫停使用者程式(stop the world)

并行收集器(parallel collector)有多條GC線程,也需要暫停使用者程式

并發收集器(concurrent collector)有一條或多條GC線程,需要在部分階段暫停使用者程式

JAVA提供了多種類型的垃圾收集器, JVM 中的垃圾收集一般都采用“分代收集”,不同的堆記憶體區域采用不同的收集算法,主要目的就是為了增加吞吐量或降低停頓時間。

1.Serial (用于新生代,采用複制算法)(串行收集器)

2.Serial Old(用于老年代,采用标記整理算法)(串行收集器)

3.ParNew(用于新生代,采用複制算法)(并行收集器)

4.Parallel Old(用于老年代,采用标記整理算法)(并行收集器)

5.Parallel Scavenge(用新生代,采用複制算法)(并行收集器)

6.CMS(用于老年代,采用标記清除算法)(并發收集器)

7.G1(jdk1.7以後的版本推出的,維持高回收率,減少停頓,類似于concurrenthashmap的分區概念)

(java9預設gc算法是G1,且把CMS标記為廢棄)

這裡着重講一下CMS,因為它是并發的,速度很快。

CMS(Concurrent Mark Sweep)收集器是一種以擷取最短回收停頓時間為目标的收集器,停頓時間短,使用者體驗就好。

基于“标記清除”算法,并發收集、低停頓,運作過程複雜,分4步:

1)初始标記:僅僅标記GC Roots能直接關聯到的對象,速度快,但是需要“Stop The World”

2)并發标記:就是進行追蹤引用鍊的過程,可以和使用者線程并發執行。

3)重新标記:修正并發标記階段因使用者線程繼續運作而導緻标記發生變化的那部分對象的标記記錄,比初始标記時間長但遠比并發标記時間短,需要“Stop The World”

4)并發清除:清除标記為可以回收對象,可以和使用者線程并發執行

1)、3)會造成停頓,由于整個過程耗時最長的并發标記和并發清除都可以和使用者線程一起工作,是以總體上來看,CMS收集器的記憶體回收過程和使用者線程是并發執行的。

JVM中将對象的引用分為四種類型,不同的對象引用類型會造成GC采用不同的方法進行回收:

(1)強引用:預設情況下,對象采用的均為強引用(這個對象的執行個體沒有其他對象引用,GC時才會被回收)

(2)軟引用:軟引用是java中提供的一種比較适合于緩存場景的應用(隻有在記憶體不夠用的情況下才會被GC)

(3)弱引用:在GC時一定會被GC回收

(4)虛引用:由于虛引用隻是用來得知對象是否被GC

jvm