閱讀843 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Java GC(絕對幹貨)

範圍:要回收哪些區域

在JVM五種內存模型中,有三個是不需要進行垃圾回收的:程序計數器、JVM棧、本地方法棧。因為它們的生命周期是和線程同步的,隨著線程的銷毀,它們占用的內存會自動釋放,所以隻有方法區和堆需要進行GC。

前提:如何判斷對象已死

所有的垃圾收集算法都麵臨同一個問題,那就是找出應用程序不可到達的內存塊,將其釋放,這裏麵講的不可達主要是指應用程序已經沒有內存塊的引用了, 在Java中,某個對象對應用程序是可到達的是指:這個對象被根(根主要是指類的靜態變量,或者活躍在所有線程棧的對象的引用)引用或者對象被另一個可到達的對象引用。

引用計數算法

引用計數是最簡單直接的一種方式,這種方式在每一個對象中增加一個引用的計數,這個計數代表當前程序有多少個引用引用了此對象,如果此對象的引用計數變為0,那麼此對象就可以作為垃圾收集器的目標對象來收集。
優點:簡單,直接,不需要暫停整個應用
缺點:1.需要編譯器的配合,編譯器要生成特殊的指令來進行引用計數的操作;2.不能處理循環引用的問題
因此這種方法是垃圾收集的早期策略,現在很少使用。Sun的JVM並沒有采用引用計數算法來進行垃圾回收,而是基於根搜索算法的。

可達性分析算法(根搜索算法)

通過一係列的名為“GC Root”的對象作為起點,從這些節點向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Root沒有任何引用鏈相連時,則該對象不可達,該對象是不可使用的,垃圾收集器將回收其所占的內存。

在java語言中,可作為GCRoot的對象包括以下幾種:
a. java虛擬機棧(棧幀中的本地變量表)中的引用的對象。
b.方法區中的類靜態屬性引用的對象。
c.方法區中的常量引用的對象。
d.本地方法棧中JNI本地方法的引用對象。

四種引用

GC在收集一個對象的時候會判斷是否有引用指向對象,在JAVA中的引用主要有四種:

強引用(Strong Reference)

強引用是使用最普遍的引用。如果一個對象具有強引用,那垃圾回收器絕不會回收它。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。

軟引用(Soft Reference)

如果一個對象隻具有軟引用,則內存空間足夠,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存。隻要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。
下麵舉個例子,假如有一個應用需要讀取大量的本地圖片,如果每次讀取圖片都從硬盤讀取,則會嚴重影響性能,但是如果全部加載到內存當中,又有可能造成內存溢出,此時使用軟引用可以解決這個問題。
設計思路是:用一個HashMap來保存圖片的路徑和相應圖片對象關聯的軟引用之間的映射關係,在內存不足時,JVM會自動回收這些緩存圖片對象所占用的空間,從而有效地避免了內存溢出的問題。
軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。

弱引用(Weak Reference)

弱引用與軟引用的區別在於:隻具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了隻具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由於垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些隻具有弱引用的對象。
弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。

虛引用(Phantom Reference)

“虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定對象的生命周期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。
虛引用主要用於檢測對象是否已經從內存中刪除,跟蹤對象被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用隊列 (ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。
虛引用的唯一目的是當對象被回收時收到一個係統通知。

finalize() 方法

通過可達性分析,那些不可達的對象並不是立即被銷毀,他們還有被拯救的機會。
如果要回收一個不可達的對象,要經曆兩次標記過程。首先是第一次標記,並判斷對象是否覆寫了 finalize 方法,如果沒有覆寫,則直接進行第二次標記並被回收。
如果對象有覆寫finalize 方法,則會將改對象加入一個叫“F-Queue”的隊列中,虛擬機會建立一個低優先級的 Finalizer 線程去執行它,這裏說的“執行”是指該線程會去觸發 finalize 方法,但是並不會等待 finalize 方法執行完成。主要是因為 finalize 方法的不確定性,它可能要花很長時間才能執行完成,甚至死循環,永遠不結束,這將導致整個 GC 工作的異常,甚至崩潰。
關於拯救,可以在 finalize 方法中將自己(this關鍵字)賦值給類變量或其他對象的成員變量,則第二次標記時它將被移出回收的集合,如果對象並未被拯救,則最終被回收。
finalize 方法隻會被調用一次,如果一個在 finalize 被拯救的對象再次需要回收,則它的 finalize 將不會再被觸發了。
不建議使用finalize 方法,它的運行代價高,不確定性大,GC 也不會等待它執行完成,它的功能完全可以被 try-finally 代替。

方法區的回收

方法區也會被回收,其被回收的內存有:廢棄常量、無用的類。
在 HotSpot 虛擬機規範裏,將永久帶作為方法區的實現。
廢棄常量:沒有被引用的常量,如 String。
判斷無用的類:
(1).該類的所有實例都已經被回收,即java堆中不存在該類的實例對象。
(2).加載該類的類加載器已經被回收。
(3).該類所對應的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射機製訪問該類的方法。

各種垃圾收集算法

標記-清除算法

步驟:
1、標記:從根集合開始掃描,標記存活對象;
2、清除:再次掃描真個內存空間,回收未被標記的對象。
此算法一般沒有虛擬機采用
優點1:解決了循環引用的問題
優點2:與複製算法相比,不需要對象移動,效率較高,而且還不需要額外的空間
不足1:每個活躍的對象都要進行掃描,而且要掃描兩次,效率較低,收集暫停的時間比較長。
不足2:產生不連續的內存碎片

標記-整理(壓縮)算法

對標記-清除算法的改進
標記過程與標記-清除算法一樣,但是標記完成後,存活對象向一端移動,然後清理邊界的內存
步驟:
1、標記:從根集合開始掃描,標記存活對象;
2、整理:再次掃描真個內存空間,並往內存一段移動存活對象,再清理掉邊界的對象。
不會產生內存碎片,但是依舊移動對象的成本。
適合老年代
還有一種算法是標記-清除-整理(壓縮),是在多次標記清除後,再進行一次整理,這樣就減少了移動對象的成本。

複製算法

將內存分成兩塊容量大小相等的區域,每次隻使用其中一塊,當這一塊內存用完了,就將所有存活對象複製到另一塊內存空間,然後清除前一塊內存空間。
此種方法實現簡單、效率較高,優點:
1、不會產生內存碎;
2、沒有了先標記再刪除的步驟,而是通過Tracing從 From內存中找到存活對象,複製到另一塊To內存區域,From隻要移動堆頂指針便可再次使用。
缺點:
1、複製的代價較高,所有適合新生代,因為新生代的對象存活率較低,需要複製的對象較少;
2、需要雙倍的內存空間,而且總是有一塊內存空閑,浪費空間。

分代收集算法

所有商業虛擬機都采用這種方式,將堆分成新生代和老年代,新生代使用複製算法,老年代使用標記-整理算法

GC 類型

1.Minor GC 針對新生代的 GC
2.Major GC 針對老年代的 GC
3.Full GC 針對新生代、老年代、永久帶的 GC

為什麼要分不同的 GC 類型,主要是1、對象有不同的生命周期,經研究,98%的對象都是臨時對象;2、根據各代的特點應用不同的 GC 算法,提高 GC 效率。

各種垃圾收集器

串行收集器(Serial Collector)

單線程,會發生停頓
適用場景:
1.單 CPU、新生代小、對停頓時間要求不高的應用
2.client 模式下或32位 Windows 上的默認收集器
新生代均采用複製算法,老年代用標記-整理算法(Serial Old Collector)
在單核 CPU 上麵的運行效果較好,甚至可能超過並行垃圾收集器,因為並行垃圾收集器有線程的切換消耗。
當 Eden 空間分配不足時觸發
原理:
1.拷貝 Eden 和 From 空間的存活對象到 To 空間
2.部分對象可能晉升到老年代(大對象、達到年齡的對象、To 空間不足時)
3.清空 Eden、From 空間,From 與 To 空間交換角色

ParNew(Serial 收集器的多線程版本)

新生代收集器,是 Serial 的多線程版,是 Server 模式下的虛擬機中首選的新生代收集器,不是默認收集器。
除了 Serial 外,是唯一能與 CMS 收集器配合工作的收集器。
多線程下,性能較好,單線程下,並不會比 Serial 好。

並行收集器(Parallel Scavenge)

特性:
1.並行、停頓
2.並行線程數:CPU <= 8 := 8,CPU > 8 := (3+ cpu * 5) / 8,也可強製指定 GC 線程數
3.自適應調節策略,如果把該策略打開,則虛擬機會自動調整新生代的大小比例和晉升老年代的對象大小、年齡等細節參數
4.吞吐量優先收集器,即可用設置一個 GC 時間,收集器將盡可能的在該時間內完成 GC

吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間),即吞吐量越高,則垃圾收集時間就要求越短

用戶可以設置最大垃圾收集停頓時間或者吞吐量
但並不是把最大垃圾收集停頓時間設置得越短越好,因為它是以犧牲吞吐量和新生代空間的代價來換取的,比如收集300M 空間總會比收集500M 空間更快,再如收集頻率加高,本來10秒收集一次,每次停頓100毫秒,但是現在改成了5秒收集一次,每次停頓70毫秒,停頓時間是小了,但是吞吐量確也降下來了。

適用場景:
1.多 CPU、對停頓時間要求高的應用
2.是 Server 端的默認新生代收集器

Serial Old

是 Serial 收集器的老年代版本,依舊是單線程收集器,采用標記-整理算法,

Parallel Old

CMS(並發-標記-清除)

CMS 是一種以獲取最短回收停頓時間為目標的收集器。
步驟:
1.初始標記
此階段僅僅是標記一下 GC Roots 能直接關聯到的對象,速度很快,但是會停頓

注意:這裏不是 GC Roots Tracing 的過程

2.並發標記
GC Roots Tracing 的過程,這個階段可以與用戶線程一起工作,不會造成停頓,從而導致整個停頓時間大大降低
3.重新標記
是為了修正並發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄
4.並發清除
優點:停頓時間短,但是總的 GC 時間長
缺點:
1.並發程序都是 CPU 敏感的,並發標記和並發清除可能會搶占應用 CPU
2.總的 GC 時間長
3.無法處理浮動垃圾

浮動垃圾:在並發清除過程中,程序還在運行,可能產生新的垃圾,但是本次 GC 確不可能清除掉這些新產生的垃圾了,所以這些新產生垃圾就叫浮動垃圾,也就是說在一次 CMS 的 GC 後,用戶獲取不到一個完全幹淨的內存空間,還是或多或少存在浮動垃圾的。

4.由於在並發標記和並發清除階段,用戶程序依舊在運行,所以也就需要為用戶程序的運行預留一定空間,而不能想其他收集器一樣會暫停用戶程序的運行。在此期間,就可能發生預留空間不足,導致程序異常的情況。
5.是基於標記-清除的收集器,所以會產生內存碎片

G1

這款開發了10多年的收集器還比較年輕,目前還很少聽說有人在生產環境使用。
此款收集器可以獨立管理整個 java heap 空間,而不需要其他收集器的配合。
步驟:
1. 初始標記
與CMS 一樣,隻是標記一下 GC Roots 能直接關聯到的對象,速度很快,但是需要停頓
2. 並發標記
GC Roots Tracing 過程,並發執行
3. 最終標記
並行執行,需要停頓
4. 篩選回收
並行執行,需要停頓

G1收集器把 Heap 分為多個大小相等的 Region,G1可以有計劃的避免進行全區域的垃圾收集。G1跟蹤各個 Region 裏麵的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後台維護一個優先列表,每次根據允許的收集時間,優先收集價值大的 Regin,保證 G1收集器在有限時間內獲取最大的收集效率。

優點:
1. 存在並發與並行操作,最大化利用硬件資源,提升收集效率
2. 分代收集,雖然 G1可以獨立管理整個 Heap,但是它還是保留了分代的概念,實際上,在分區時,這些區域(regions)被映射為邏輯上的 Eden, Survivor, 和 old generation(老年代)空間,使其有目的的收集特定區域的內存。
title
3. 空間整合,G1回收內存時,是將某個或多個區域的存活對象拷貝至其他空區域,同時釋放被拷貝的內存區域,這種方式在整體上看是標記-整理,在局部看(兩個 Region 之間)是複製算法,所以不會產生內存碎片
4. 可預測的停頓時間

內存分配策略

  1. 對象優先在 Eden 區分配
  2. 大對象直接進入老年代
  3. 長期存活的對象將進入老年代
  4. 動態對象年齡判斷。並不是新生代對象的年齡一定要達到某個值,才會進入老年代。Survivor空間中相同年齡所有對象大小的總和大於 Survivor 空間的一半,那麼年齡等於或大於該年齡的對象就直接進入老年代,無須等待設置的年齡
  5. 空間分配擔保

最後更新:2017-05-27 16:31:15

  上一篇:go  域名解析出現錯誤怎麼辦
  下一篇:go  《精通Spring MVC 4》——2.11 小結