[TOC]html
對象探活的目的在於找到那些須要清理的對象java
對象探活常見方法有引用計數和可達性分析等。使用引用計數法(相似於 C++ 中的智能指針 shared_ptr
)實現對象探活相對容易,但沒法解決對象之間相互循環引用的問題,例如算法
class A{ public A element; ... } A a, b; a.element = b; b.element = a;
Java 中可達性分析與 GC Roots 是息息相關的。Java 是面向對象的語言,全部對象之間都使用引用進行關聯(底層實現通常是 C/C++ 中的指針)。從軟件啓動開始,全部的對象都會有一個「父對象」,全部的對象都是由父對象建立的,能夠認爲 JVM 是始祖對象。Java 啓動且未進入main 函數前初始化的對象能夠認爲其父對象是 JVM;進入 main 函數後建立的全部對象能夠認爲其父對象是 main 函數所在的對象。因而可知 Java 中全部對象之間的關係可使用多叉樹進行表示,這些樹的根節點就是垃圾回收掃描的起點(不考慮 JVM),也就是 GC Roots安全
全部對象的父對象均可以追溯到 JVM,但垃圾回收時不將 JVM 當作起點數據結構
GC Roots 通常有如下 4 類:多線程
Java 1.2 後引用被分爲如下 4 類,「引用強度」由強到弱分別爲:併發
強引用就是指在程序代碼之中廣泛存在的,相似
Object obj= new Object()
這類的引用,只要強引用還存在,垃圾收集器就不會回收掉被引用的對象框架
軟引用是用來描述一些還有用但並不是必需的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收(可使用 SoftReference 函數建立)函數
弱引用也是用來描述非必需對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前(WeakReference)oop
爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知(PhantomReference)
垃圾回收會涉及到對象探活與內存整理。由於 GC 過程很難保證正在移動的對象沒有被其餘執行中的線程所引用與修改,因此這個過程通常須要整個 JVM 中止執行工做線程,Sun 將這件事稱爲 Stop The World
。虛擬機暫停執行工做線程會影響業務性能,因此要儘量減小垃圾回收形成的系統停頓時間。使用句柄來訪問對象時能夠只停頓與將要 GC 的對象相關聯的線程,其餘線程照樣執行,但使用句柄訪問對象效率不高,這類方法將 GC 時間分攤到了工做進程執行過程
根據對象存活週期的不一樣將內存劃分爲幾塊。通常把 Java 堆分爲新生代(剛建立沒多久的對象)和老年代(已經存在好久的對象,一些比較大的對象也默認是老年代),這樣就能夠根據各個年代的特色採用最適當的收集算法。例如新生代對象在每次垃圾回收時只有少許對象存活,可使用下面說起的複製算法;對於存活時間長的老年代對象可使用下面說起的「標記-清理」或者「標記-整理」算法
具體過程和下面的兩次標記過程相似,缺點是效率低且會出現內存碎片問題
若是對象在進行可達性分析後發現沒有與 GC Roots 相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize() 方法。當對象沒有覆蓋 finalize() 方法,或者 finalize() 方法已經被虛擬機調用過期,虛擬機將對象視爲「沒有必要執行」,不然是「有必要執行」
若是這個對象被斷定爲有必要執行 finalize() 方法,那麼這個對象將會放置在一個叫作 F-Queue 的隊列之中,並在稍後由一個由虛擬機自動創建的、低優先級的 Finalizer 線程去執行它。這裏所謂的「執行」 是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束
finalize() 方法是對象逃脫死亡命運的最後一次機會,稍後 GC 將對 F-Queue 中的對象進行第二次小規模的標記,若是對象要在 finalize() 中成功拯救本身——只要從新與引用鏈上的任何一個對象創建關聯,那麼對象將被移出 F-Queue,並被認爲「活着」
對象的 finalize() 方法只會被調用一次,在 Java 中儘可能不要使用 finalize 方法
標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存
基礎的複製算法將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲了原來的一半,代價比較大
從統計上來說,在進行一次垃圾回收前,有 98% 的新生代對象都是可回收的,因此不必把內存分爲大小相等的兩份
大部分商用虛擬機使用下述方法回收新生代對象
HotSpot 虛擬機將新生代內存分爲三個部分:80% 的 Eden 和兩個 10% 的 Survivor,建立對象時優先在 Eden 中建立。每次垃圾回收時由於 98% 的對象均可以刪除,因此大部分狀況下 10% 的內存便可存儲全部存活的對象。垃圾回收前虛擬機只會使用 Eden 和其中一個 Survivor,回收時將 Eden 和被使用的 Survivor 中的對象移到另外一個未被使用的 Survivor 中
當一個 Survivor 保存不了餘下的對象時,會觸發分配擔保(下面有介紹)
類(方法,位於靜態內存區)須要同時知足三個條件纔算可回收類
ClassLoader
已經被回收java.lang.Class
對象沒有在任何地方被引用,即沒法在任何地方經過反射訪問該類的方法對方法是否進行回收,虛擬機提供了配置選項。在大量使用反射、動態代理、CGLib
等 ByteCode
的框架、動態生成 JSP 以及 OSGi 這類頻繁自定義 ClassLoader
的場景都須要虛擬機具有類卸載的功能,以保證靜態區不會溢出
JVM 的垃圾回收分爲保守式 GC 和準確式 GC,這裏的保守和準確指的是 JVM 在垃圾回收時是否肯定當前字段爲引用類型。下面作詳細的介紹,參考
保守式 GC
保守式 GC, JVM 不記錄變量類型信息。每次進行垃圾回收時掃描全部 GC Roots 區域(本地方法棧、JVM 棧、靜態方法區等),若是發現一個疑似指針變量(如真實的指針、整數等),JVM 都會檢查堆中是否存在對象,若是存在就將對象信息保存在內存分配表中以標識某一段內存已被對象佔用,不然就從內存分配表中刪除這段內存以供下次分配。若是恰巧一個整數類型和一個指針所指向的地址相同,不管這個對象是否還「存活」,保守式 GC 都會保留這塊內存,由於 JVM 不能肯定這個變量不是指針。由於同一個對象可能在不一樣的棧幀中被使用且 JVM 又沒法區分變量類型,因此 JVM 不能修改棧中「地址」的值(萬一一個整型值和指針的值恰好相同),故保守式 GC 不能移動對象,只能使用相似標記-清除的算法法回收內存。若是 JVM 使用句柄(另外一種爲直接內存訪問)的方式訪問對象,則保守式 GC 也能夠移動對象,但本該回收的對象依舊存在的現象沒法消除
半保守式 GC
JVM 中棧上的變量通常不包含類型信息,但堆中的對象能夠包含類型信息(例如反射等,這個和 C++ 中的虛函數表概念相似),因此垃圾回收時堆對象中的指針類型是能夠肯定的,此時就不存在上面整型和指針類型不分的狀況,堆上的指針能夠實現準確式回收,且能夠移動對象
準確式 GC
準確式 GC 指 JVM 進行內存回收時能夠肯定指針的類型與位置,這通常須要輔助的數據結構與存儲空間,HotSpot 中這些數據存儲在 OopMap 中
OopMap 是 HotSpot 實現準確式垃圾回收的基礎,HotSpot 中的 GC Roots 通常保存在 OopMap 中,這樣 JVM 在 GC 時就不用掃描全部靜態區和棧幀,棧幀在運行時動態變化,因此 OopMap 的內容也在不斷的變化
在類文件載入和 JIT(Just-In-Time Compiler)編譯過程當中 OopMap 都有發生變化的可能
何時或者說代碼有什麼特色時應該更新 OopMap?總不能編譯器每執行一條指令就判斷一下是否須要更新 OopMap吧,這樣效率過低了
GC 須要在更新 OopMap 後才能進行, GC 時的 OopMap 已經包含了全部 GC Roots,且對象之間的引用關係不會在 GC 過程當中發生變化,因此 JVM 並不能隨意在任何位置進行垃圾回收,JVM 進行垃圾回收的位置須要知足必定的條件,這些知足條件的位置被稱爲安全點。常見的安全點有:方法調用、循環跳轉、異常跳轉等,這些點都不會改變對象之間的引用關係
垃圾回收須要全部線程都運行到安全點,通常有兩種方式
搶先式中斷(少見)
JVM 中止全部線程,若是一個線程沒有運行到安全點就恢復其運行,直至到達安全點
主動式中斷(常見)
JVM 設置一個標誌位,當線程到達安全點時檢查這個標誌並自動中止運行
若是線程阻塞了,或者 sleep,線程將沒法執行到安全點,則 JVM 沒法進行 GC,此時就須要安全區域的概念。所謂的安全區域,是指引用關係不會發生變化的指令段。線程進入安全區域時會給出一個標識,以供 JVM 查詢
從垃圾回收器出現至今並無出現一款通用的、在任何場景下性能都很是出衆的實現,因此大部分請求下須要在不一樣的場景下選擇不一樣的垃圾回收器,具體場景具體分析
單線程垃圾收集器,收集時須要中止全部工做線程
Serial 的多線程版本,能夠配合 CMS 使用
使用複製算法的多線程收集器。其餘算法關注如何減小垃圾回收時間,當前算法控制垃圾回收時間和工做線程工做時間的比值。你能夠設置一個小的比值,則新生代會佔用更多的內存;你能夠設置一個大的比值,新生代會佔用更少的內存,固然回收時間會變長
CMS(Concurrent Mark Sweep)是一種追求最短停頓爲目標的收集器,特色是併發低停頓收集器,老年代推薦使用
CMS 能夠在用戶進程運行時進行垃圾回收,此時用戶進程產生的垃圾被稱爲浮動垃圾,CMS 只能等待下次回收時回收這些垃圾
通常在老年代內存被佔用超過必定比例時纔會觸發 CMS 垃圾回收,提升比例能夠減小垃圾回收的次數
Serial 的老年代版本
Parallel Scavenge 的老年代版本
將來可能替代 CMS,G1 能夠同時用於新生代與老年代
不一樣垃圾回收器的日誌格式不一樣,但有必定的共性
33.125:[ GC[ DefNew: 3324K- > 152K( 3712K), 0. 0025925 secs] 3324K- > 152K( 11904K), 0. 0031680 secs] 100.667:[ Full GC[ Tenured: 0 K- > 210K( 10240K), 0. 0149142secs] 4603K- > 210K( 19456K),[ Perm: 2999K- > 2999K( 21248K)], 0. 0150007 secs][ Times: user= 0. 01 sys= 0. 00, real= 0. 02 secs]
以第一行爲例: