finalize()能作的全部工做,使用try-finally或者其餘方式均可以作得更好、更及時,因此筆者建議你們徹底能夠忘掉Java語言中有這個方法的存在。html
——《深刻理解JVM》算法
finalize()方法確實能夠實現一次對象的自救,可是其不肯定性和昂貴的運行代價都代表這個方法的使用須要十分的慎重。那麼finalize()在什麼時期起做用又是如何實現對象的自救的呢?首先咱們要理解虛擬機在掃描到死亡對象的時候並非直接回收的,而是進行一次標記而且篩選,篩選的條件就是其對象的finalize方法是否有必要執行。若是當前對象沒有重寫finalize方法或者已經調用過一次finalize方法,那麼則視爲沒有必要執行,此時便失去自救的機會,放入"即將回收"集合中。多線程
不然的話,則將對象放入一個叫F-Queue的隊列中,稍後虛擬機將一個個的執行隊列中對象的finalize方法(就是在此處對象能夠在finalize方法中將自身關聯到引用鏈,從而暫時逃脫被回收的命運),須要注意的是虛擬機保證執行但不保證執行完finalize方法,緣由是若是finalize方法執行時間過長或者陷入死循環,則可能讓系統奔潰。所有執行以後,虛擬機將對隊列的對象從新標記一次,若是還不在引用鏈中則GG,不然將其移出"即將回收"集合。下面例子參考《深刻理解JVM》實現自救而且驗證只能自救一次的過程。併發
public class TestForGc { /** 定義一個根節點的靜態變量 */ public static TestForGc INSTANCE; /** * 重寫finalize方法,讓其被標記爲有必要執行而且加入F-Q * * @throws Throwable */ @Override protected void finalize() throws Throwable { super.finalize(); System.err.println("finalize method in TestForGc Class invoked!"); // 將自身關聯到根節點中,實現自救 INSTANCE = this; } public static void main(String[] args) throws InterruptedException { INSTANCE = new TestForGc(); INSTANCE = null; System.gc(); // 睡眠1S,保證F-Q中的方法執行完畢 TimeUnit.SECONDS.sleep(1); if (Objects.nonNull(INSTANCE)) { System.out.println("i successfully save myself by finalize method!"); } else { System.out.println("i am dead :("); } /* * 下面驗證finalize方法只能調用一次 * 幾乎徹底同樣的代碼,倒是不一樣的結局 */ INSTANCE = null; System.gc(); // 睡眠1S TimeUnit.SECONDS.sleep(1); if (Objects.nonNull(INSTANCE)) { System.out.println("i successfully save myself by finalize method again!"); } else { System.out.println("couldn't invoke finalize again, i am dead :("); } } }
執行結果:
若是說回收算法是接口,那麼垃圾回收器就是這些接口的實現類,共有7種回收器,接下來一一羅列。ide
Serial是一種單線程垃圾回收器,在工做的時候的時候會暫停全部的用戶線程,也就是"stop-the-world",雖然單線程表明了用戶線程的停頓,可是也意味着其不用進行線程的交互從而有更高的收集 效率。Serial採用複製算法,是Client端新生代的默認垃圾回收器。其工做圖相似於:學習
ParNew是Serial回收器的多線程版本,是Server端新生代的默認回收器,除了並行多線程以外,其餘包括實現都是如出一轍,固然也是採用複製算法。還有一點重要的是,新生代的收集器除了Serial以外,只有ParNew能跟年老代的CMS合做,其在低CPU的狀況下效率比Serial低,可是在多個CPU的狀況下要好的多。其工做圖:this
跟ParNew相似,做用於新生代,並行多線程而且也是採用複製算法。可是其關注的點卻不一樣,其着重的是一種叫作"吞吐量"的東西。所謂的"吞吐量"=運行用戶代碼的時間 / (運行用戶代碼的時間 + GC時間),也就是說其更加註重用戶代碼運行時間而不是減小GC停頓時間。相對於其餘收集器來講,能夠更加高效的利用CPU,更加適合做爲在後臺運算而不大須要交互的任務。Parallel收集器提供了兩個比較重要的參數。spa
-XX:MaxGCPauseMillis:表示收集器將盡量的在這個參數設定的毫秒數內完成回收工做。但這並不表明其設置的越低越好,縮減回收時間是經過減小吞吐量換來的,若是設置得過低可能致使頻繁的GC。線程
-XX:GCTimeRatio:表示代碼運行時間和垃圾回收時間的比率,好比說設置爲19,那麼則垃圾回收時間佔比爲 1 / (1+19) = 5%,默認是99。3d
Serial的年老代版本,同Serial基本類似,不一樣的是採用的是標記-整理算法實現,做爲Client端默認的年老代收集器。若是在Server端的話,那麼其主要做用有二:
一、跟新生代的Parallel Scavenge收集器配合。
二、作一個有價值的"備胎":當CMS垃圾回收器由於預留空間問題放不下對象而發生Concurrent Mode Fail時,做爲其備選方案執行垃圾回收。
Parallel Scavenge的年老代版本,多線程並行,一樣注重吞吐量,使用標記-整理算法。這個收集器能夠跟新生代的Parallel Svavenge一塊兒搭配使用,在注重吞吐量和CPU資源敏感的場合中是一對很好的組合。
來了,它來了!CMS垃圾回收器被當作是具備劃時代意義的、真正實現併發的垃圾回收器,總而言之=》
,--^----------,--------,-----,-------^--,
| ||||||||| `--------' | O
`+---------------------------^----------|
`\_,-------, _______________________強__|
/ XXXXXX /`| /
/ XXXXXX / `\ /
/ XXXXXX /\______(
/ XXXXXX /
/ XXXXXX /
(________(
`------'
CMS是一款併發的垃圾回收器,但並不表明全程都不須要停頓,只是大部分時間是跟用戶線程一塊兒執行的。其整個GC過程當中總共有4個階段。
一、初始標記:簡單的標記全部的根節點,須要暫停全部的用戶線程,即"stop-the-world",耗時較短。關於GCRooots的過程能夠看下另外一篇文章——垃圾回收(一)。
二、併發標記:跟用戶線程一塊兒工做,尋找堆中的死亡對象,整個過程耗時最長。
三、從新標記:再次掃描,主要對象是併發標記過程當中又新增的對象,也就是驗漏。多線程,須要STW,時間相對併發標記來講短。
四、併發清除:GC線程跟用戶線程一塊兒執行,清除標記的死亡對象,"浮動垃圾"在此階段產生。
然而,優秀如CMS也會有不足之處,總共四個階段的標記及清除算法的實現一定爲其帶來一些使用的麻煩。
缺點:
一、佔用必定CPU資源:其有兩個階段須要併發跟用戶線程一塊兒執行,也就是說要跟用戶線程搶佔CPU的時間片,會佔用必定的CPU資源,若是CPU資源不太優質的狀況下,可能會形成不小的影響。
二、空間利用率不能達到最大:因爲併發清除時用戶線程也在運行,那麼在GC結束前一定會產生一些額外的垃圾,那麼就必須給這些垃圾預留必定的空間,不然會致使內存不足從而報"Concurrent Mode Failure",此時虛擬機便啓用後備方案——使用Serial Old來進行垃圾回收,進而浪費更多的時間。
三、內存碎片致使提早FullGC:CMS採用的是標記-清除算法,也就是說會產生內存碎片,那麼可能出現大對象放不下的狀況,進而不得不提早進行一次FullGC。爲了解決這個問題,虛擬機提供了兩個參數-XX:+UseCMSCompactAtFullCollection和-XX:CMSFullGCsBeforeCompaction,分別表示CMS頂不住要進行FullGC的時候進行內存的整理(整理的過程當中沒法併發,停頓時間不得不變長) 和進行多少次不壓縮的FullGC以後來一次整理的GC(默認0次,表示每次都進行內存整理)。
G1是一個新秀垃圾回收器,被賦予了很大的使命——取代CMS。G1做爲新時代的垃圾回收器,相對於其餘垃圾回收器來講有許多優點。
一、並行和併發:G1能夠利用如今的硬件優點,縮短GC時stop-the-world的停頓時間,而且GC的時候同時也能讓用戶線程執行。
二、分代收集:跟其餘垃圾回收器不一樣,G1沒有物理上的年老代和新生代,其將內存分紅了多個獨立的Region,每一個Region均可能表示屬於新生代仍是年老代,因此不須要一堆Region湊放在一塊兒而後將這塊區域稱做新生代,它們之間並不須要連續,因此只有概念上的分代,也是這種分代方式使得G1能夠獨立管理這個堆空間,不須要跟其餘回收器合做。
三、空間整合:G1的算法從Region層面看屬於複製算法(從一個Region複製到另外一個),可是從總體看又是標記-整理法。然而不論是哪一種,都表示G1不會產生內存碎片,不會由於空間不連續放不下大對象而出現FullGC的狀況。
G1回收器將內存空間分紅若干個Region,而且這些Region以前相互獨立。可是咱們都知道這並不能真正的獨立,由於一個Region中的對象不必定只會被當前Region的其餘對象引用,而可能被堆中的其餘對象引用,那G1是如何實現避免全堆掃描的呢?這個問題在分代的其餘回收器中也有,可是在這裏突顯得更加明顯而已。再G1中,對象自己都會有一個Remembered Set,這個Set存放着當前對象被其餘區域對象引用的信息,這樣子,在掃描引用的時候加上這個Set就能夠避免全堆掃描了。
具體實現大體爲:虛擬機在發現程序正在進行對Reference類型的寫操做時,會暫時中斷寫操做,而後檢查Reference引用的對象是否處於不一樣的區域(若是是分代,則只對年老代的對象進行檢查,檢查是否引用的對象在新生代),若是是的話則將引用信息記錄在被引用的Remembered Set中,這樣在GC的時候加上Remembered Set的掃描就能夠避免全堆掃描了。
跟CMS類型,G1也有四個階段(不算Remembered Set的掃描),雖然類似可是仍是有些區別的。
一、初始標記:標記可達的根節點,STW,單線程,時間短。
二、併發標記:跟用戶線程同時執行,併發執行時對象可能會產生引用變化,其會將這些變化記錄在Remembered Set Logs中,待下個階段整合。
三、最終標記:驗漏,將併發標記階段的引用變化記錄Remembered Set Logs整合到Remembered Set中。
四、篩選回收:對各個Region中的回收價值進行排序,而後執行回收計劃。暫停用戶線程,並行執行。
本文首先介紹了「對象自救」的方法——finalize,而且用一個小例子演示了對象如何實現自救。接着介紹了7種不一樣的垃圾回收器,新生代中有單線程的Serial能夠做爲Client端新生代的默認回收器,有多線程版本的Serial——ParNew,還有着重點不一樣(吞吐量)的Parallel Scavenge;年老代方面有單線程的Serial Old、跨時代意義的併發回收器——CMS,雖然優秀仍是其使用的算法和實現致使了它的三個缺點、還有吞吐量年老代版本——Parallel Old收集器,最後還簡單介紹了G1收集器的幾個過程還有獨立的Region間是如何實現避免堆掃描的。
總體下來整篇行文還有些粗糙,往後會慢慢的圓潤,若是有關於這方面好的文章能夠在下面評論區分享學習一下,下方爲各個垃圾回收器的搭配圖。
It helps me a lot if you could share your opinion with us.