2 回收無效對象的過程
當經可達性算法篩選出失效的對象之後,并不是立即清除,而是再給對象一次重生的機會
判斷是否覆寫finalize()
未覆寫該或已調用過該方法,直接釋放對象記憶體
已覆寫該方法且還未被執行,則将finalize()扔到F-Queue隊列中
執行F-Queue中的finalize()
虛拟機會以較低的優先級執行這些finalize(),不會確定所有的finalize()都會執行結束
如果finalize()中出現耗時操作,虛拟機就直接停止執行,将該對象清除
對象重生或死亡
如果在執行finalize()方法時,将this賦給了某一個引用,則該對象重生
如果沒有,那麼就會被垃圾收集器清除
注意:強烈不建議使用finalize()進行任何操作! 如果需要釋放資源,請用try-finally或者其他方式都能做得更好. 因為finalize()不确定性大,開銷大,無法保證各個對象的調用順序.
以下代碼示例看到:一個對象的finalize被執行,但依然可以存活
運作結果
3 方法區的記憶體回收
使用複制算法實作堆的記憶體回收,堆被分為新生代和老年代
新生代中的對象"朝生夕死",每次垃圾回收都會清除掉大量對象
老年代中的對象生命較長,每次垃圾回收隻有少量的對象被清除
由于方法區中存放生命周期較長的類資訊、常量、靜态變量.
是以方法區就像堆的老年代,每次GC隻有少量垃圾被清除.
方法區中主要清除兩種垃圾
廢棄常量
無用類
回收廢棄常量和回收對象類似,隻要常量池中的常量不被任何變量或對象引用,那麼這些常量就會被清除.
判定無用類的條件則較為苛刻
該類所有執行個體都已被回收
即Java堆不存在該類的任何執行個體
加載該類的ClassLoader已被回收
該類的java.lang.Class對象沒有被任何對象或變量引用,無法通過反射通路該類的方法
隻要一個類被虛拟機加載進方法區,那麼在堆中就會有一個代表該類的對象:java.lang.Class.這個對象在類被加載進方法區的時候建立,在方法區中該類被删除時清除.
4 垃圾收集算法
最基礎的收集算法,後續算法也都是基于此并改進其不足而得.
該算法會從每個GC Roots出發,依次标記有引用關系的對象,最後将沒有被标記的對象清除
把死亡對象所占據的記憶體标記為空閑記憶體,并記錄在一個空閑清單(free list)之中
當需要建立對象時,記憶體管理子產品便會從該空閑清單中尋找空閑記憶體,并劃分給建立的對象。

清除這種回收方式的原理及其簡單,但是有兩個缺點
由于Java虛拟機的堆中對象必須是連續分布的,是以可能出現總空閑記憶體足夠,但是無法配置設定的極端情況。
如果是一塊連續的記憶體空間,那麼我們可以通過指針加法(pointer bumping)來做配置設定
而對于空閑清單,Java虛拟機則需要逐個通路清單中的項,來查找能夠放入建立對象的空閑記憶體。
第二種是壓縮(compact),即把存活的對象聚集到記憶體區域的起始位置,進而留下一段連續的記憶體空間。這種做法能夠解決記憶體碎片化的問題,但代價是壓縮算法的性能開銷。
這種算法會帶來大量的空間碎片,導緻需要配置設定一個較大連續空間時容易觸發FullGC,降低了空間使用率.
為了解決這個問題,又提出了“标記-整理算法”,該算法類似計算機的磁盤整理,首先會從GC Roots出發标記存活的對象,然後将存活對象整理到記憶體空間的一端,形成連續的已使用空間,最後把已使用空間之外的部分全部清理掉,這樣就不會産生空間碎片的問題
把記憶體區域分為兩等分,分别用兩個指針from和to來維護,并且隻是用from指針指向的記憶體區域來配置設定記憶體。
當發生垃圾回收時,便把存活的對象複制到to指針指向的記憶體區域中,并且交換from指針和to指針的内容。複制這種回收方式同樣能夠解決記憶體碎片化的問題,但是它的缺點也極其明顯,即堆空間的使用效率極其低下。
将記憶體分成大小相等兩份,隻将資料存儲在其中一塊上
當需要回收時,首先标記廢棄資料
然後将有用資料複制到另一塊記憶體
最後将第一塊記憶體空間全部清除
這種算法避免了空間碎片,但記憶體縮小了一半.
每次都需将有用資料全部複制到另一片記憶體,效率不高
堆記憶體空間分為較大的Eden和兩塊較小的Survivor,每次隻使用Eden和Survivor區的一塊。這種情形下的“ Mark-Copy"減少了記憶體空間的浪費。“Mark-Copy”現作為主流的YGC算法進行新生代的垃圾回收。
在新生代中,由于大量對象都是"朝生夕死",也就是一次垃圾收集後隻有少量對象存活
是以我們可以将記憶體劃分成三塊
Eden、Survior1、Survior2
記憶體大小分别是8:1:1
配置設定記憶體時,隻使用Eden和一塊Survior1.
當發現Eden+Survior1的記憶體即将滿時,JVM會發起一次Minor GC,清除掉廢棄的對象,
并将所有存活下來的對象複制到另一塊Survior2中.
接下來就使用Survior2+Eden進行記憶體配置設定
通過這種方式,隻需要浪費10%的記憶體空間即可實作帶有壓縮功能的垃圾收集方法,避免了記憶體碎片的問題.
準備為一個對象配置設定記憶體時,發現此時Eden+Survior中空閑的區域無法裝下該對象
就會觸發MinorGC(新生代 GC 算法),對該區域的廢棄對象進行回收.
但如果MinorGC過後隻有少量對象被回收,仍然無法裝下新對象
那麼此時需要将Eden+Survior中的所有對象都轉移到老年代中,然後再将新對象存入Eden區.這個過程就是"配置設定擔保".
在發生 minor gc 前,虛拟機會檢測老年代最大可用連續空間是否大于新生代所有對象總空間
若成立,minor gc 可確定安全
若不成立,JVM會檢視 HandlePromotionFailure是否允許擔保失敗
若允許
那麼會繼續檢測老年代最大可用的連續空間是否 > 曆次晉升到老年代對象的平均大小
若大于
則将嘗試進行一次 minor gc,盡管這次 minor gc 是有風險的
若小于或 HandlePromotionFailure 設定不允許冒險
改為進行一次 full gc (老年代GC)
在回收前,标記過程仍與"清除"一樣
但後續不是直接清理可回收對象,而是
将所有存活對象移到一端
直接清掉端邊界之外記憶體
這是一種老年代垃圾收集算法.
老年代中對象一般壽命較長,每次垃圾回收會有大量對象存活
是以如果選用"複制"算法,每次需要較多的複制操作,效率低
而且,在新生代中使用"複制"算法
當 Eden+Survior 都裝不下某個對象時,可使用老年代記憶體進行"配置設定擔保"
而如果在老年代使用該算法,那麼在老年代中如果出現 Eden+Survior 裝不下某個對象時,沒有其他區域給他作配置設定擔保
是以,老年代中一般使用"壓縮"算法
目前商業虛拟機都采用此算法.
根據對象存活周期的不同将Java堆劃分為老年代和新生代,根據各個年代的特點使用最佳的收集算法.
老年代中對象存活率高,無額外空間對其配置設定擔保,必須使用"标記-清除"或"标記-壓縮"算法
新生代中存放"朝生夕死"的對象,用複制算法,隻需要付出少量存活對象的複制成本,就可完成收集
5 Java中引用的種類
根據生命周期的長短,将引用分為4類
我們平時所使用的引用就是強引用
類似A a = new A();
即通過關鍵字new建立的對象所關聯的引用就是強引用
隻要強引用還存在,該對象永遠不會被回收 ,即使發生了OOM!是以也是記憶體洩漏的主要原因之一。
一些還有用但并非必需的對象。
隻有當堆即将發生OOM時,JVM才會回收軟引用所指向的對象.
即記憶體充足時不會回收,不夠時才會回收。
軟引用通過SoftReference類實作
軟引用的生命周期比強引用短一些
也是描述非必需對象,比軟引用更弱
所關聯的對象隻能存活到下一次GC發生前.
隻要垃圾收集器工作,無論記憶體是否足夠,弱引用所關聯的對象都會被回收.
弱引用通過WeakReference類實作.
也叫幽靈(幻影)引用,最弱的引用關系.
它和沒有引用沒有差別,無法通過虛引用取得對象執行個體.
設定虛引用唯一的作用就是在該對象被回收之前收到一條系統通知.
虛引用通過PhantomReference類來實作.
總結
Java虛拟機中的垃圾回收器采用可達性分析來探索所有存活的對象。它從一系列GC Roots出發,邊标記邊探索所有被引用的對象。
為了防止在标記過程中堆棧的狀态發生改變,Java虛拟機采取安全點機制來實作Stop-the-world操作,暫停其他非垃圾回收線程。
回收死亡對象的記憶體共有三種方式,分别為:會造成記憶體碎片的清除、性能開銷較大的壓縮、以及堆使用效率較低的複制。
今天的實踐環節,你可以體驗一下無安全點檢測的計數循環帶來的長暫停。你可以分别測單獨跑foo方法或者bar方法的時間,然後與合起來跑的時間比較一下。