垃圾回收
斷定對象存活
引用計數算法:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1,當引用失效時,計數器值就減1。任什麼時候刻計數器爲0的對象就是不能再被使用的。客觀地說,引用計數算法( Reference Counting)的實現簡單,斷定效率也很高,在大部分狀況下它都是一個不錯的算法。可是,至少主流的Java虛擬機裏面沒有選用引用計數算法來管理內存,其中最主要的緣由是它很難解決對象之間相互循環引用的問題。html
在主流的商用程序語言(Java、C#,甚至包括前面提到的古老的Lisp)的主流實現中,都是稱經過可達性分析( Reachability Analysis)來斷定對象是否存活的。這個算法的基本思路就是經過一系列的稱爲「 GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象與任何「GC Roots」都不存在引用鏈相連(用圖論的話來講,就是從「GC Roots」到這個對象不可達)時,則證實此對象是不可用的。java
所謂「GC roots」,或者說tracing GC的「根集合」,就是一組必須活躍的引用。 例如說,這些引用可能包括:程序員
- 全部Java線程當前活躍的棧幀裏指向GC堆裏的對象的引用;換句話說,當前全部正在被調用的方法的引用類型的參數/局部變量/臨時值。
- VM的一些靜態數據結構裏指向GC堆裏的對象的引用,例如說HotSpot VM裏的Universe裏有不少這樣的引用。
- JNI handles,包括global handles和local handles
- (看狀況)全部當前被加載的Java類
- (看狀況)Java類的引用類型靜態變量
- (看狀況)Java類的運行時常量池裏的引用類型常量(String或Class類型)
- (看狀況)String常量池(StringTable)裏的引用
注意,是一組必須活躍的引用(在JDK1.2以後,Java對引用的概念進行了擴充,將引用分爲強引用( Strong Reference)、軟引用( Soft Reference)、弱引用( Weak Reference)、虛引用( Phantom Reference)。),不是對象。算法
做者:RednaxelaFX 連接:https://www.zhihu.com/question/53613423/answer/135743258編程
引用
不管是經過引用計數算法判斷對象的引用數量,仍是經過可達性分析算法判斷對象的引用鏈是否可達,斷定對象是否存活都與「引用」有關。在JDK1.2之前,Java中的引用的定義很傳統也很簡略:若是 reference 類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。一個對象在這種定義下只有被引用或者沒有被引用兩種狀態。但咱們但願能描述這樣一類對象:當內存空間還足夠時,則能保留在內存之中;若是內存空間在進行垃圾收集後仍是很是緊張,則能夠拋棄這些對象。api
因而在JDK1.2以後,Java對引用的概念進行了擴充,將引用分爲強引用( Strong Reference)、軟引用( Soft Reference)、弱引用( Weak Reference)、虛引用( Phantom Reference)4種,這4種引用強度依次逐漸減弱。安全
- 強引用就是指在程序代碼之中廣泛存在的,相似
Object obj= new Object()
這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。 - 軟引用是用來描述一些還有用但並不是必需的對象。對於軟引用關聯着的對象,**在系統將要發生內存溢出異常前,將會把這些對象列進回收範圍之中進行第二次回收。**若是此次回收尚未足夠的內存,纔會拋出內存溢出異常。在JDK1.2以後,提供了
SoftReference
類來實現軟引用。 - 弱引用也是用來描述非必需對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK1.2以後,提供了
WeakReference
類來實現弱引用。 - 虛引用也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知。在JDK1.2以後,提供了
PhantomReference
類來實現虛引用。
finalize 方法與兩次標記
即便在可達性分析算法中不可達的對象,也並不是是「非死不可」的,這時候它們暫時處於「緩刑」階段。數據結構
若是某實例在進行可達性分析後被第一次發現沒有與 GC Roots 相鏈接的引用鏈,則標記而且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize()
方法。當對象沒有覆蓋 finalize()
方法,或者 finalize()
方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲「沒有必要執行finalize()
方法」。多線程
若是這個對象被斷定爲有必要執行 finalize()
方法,那麼這個對象將會放置在一個叫作F-Queue的隊列之中,並在稍後由一個由虛擬機自動創建的、低優先級的 Finalizer 線程去執行它。這裏所謂的「執行」是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束,這樣作的緣由是,若是一個對象在 finalize 方法中執行緩慢(甚至發生了死循環),將極可能會致使 F-Queue隊列中其餘對象永久處於等待,甚至致使整個內存回收系統崩潰。併發
finalize()
方法是對象逃脫死亡命運的最後一次機會。稍後GC將對 F-Queue 中的對象進行第二次小規模的標記,若是對象想要拯救拯救本身,必須在 finalize()
方法中與引用鏈上的任何一個對象創建關聯,那樣第二次標記時它將被移除出「即將回收」的集合。不然基本上它就真的被回收了。
若是一個對象被斷定爲沒有必要執行 finalize 方法,那就直接被可回收。
任何一個對象的 finalize 方法都只會被系統自動調用一次,若是對象曾經由於這個方法死裏逃生,那麼它第二次面臨緩刑這個方法也救不了它。
須要特別說明的是,筆者並不鼓勵你們使用上述方法反而建議你們儘可能避免使用它,由於它不是C++中的析構函數,而是Java剛誕生時爲了使C++程序員更容易接受它所作出的一個妥協。它的運行代價高昂,不肯定性大,沒法保證各個對象的調用順序。有些教材中描述它適合作「關閉外部資源」之類的工做,這徹底是對這個方法用途的一種自我安慰。finalize()
能作的全部工做,使用try-finally
或者其餘方式均可以作得更好、更及時,因此筆者建議你們徹底能夠忘掉Java語言中有這個方法的存在。
方法區的垃圾收集
不少人認爲方法區(或者 HotSpot 虛擬機中的永久代)是沒有垃圾收集的,Java虛擬機規範中確實說過能夠不要求虛擬機在方法區實現垃圾收集,並且在方法區中進行垃圾收集的「性價比」通常比較低:在堆中,尤爲是在新生代中,常規應用進行一次垃圾收集通常能夠回收70%~95%的空間,而永久代的垃圾收集效率遠低於此。
永久代(方法區)的垃圾收集主要回收兩部份內容:廢棄常量和無用的類。回收廢棄常量以常量池中字面量的回收爲例,假如一個字符串「abc」已經進入了常量池中,可是當前系統沒有任何一個String對象引用常量池中的「abc」常量,也沒有其餘地方引用了這個字面量,若是這時發生內存回收,並且必要的話,這個「abc」常量就會被系統清理出常量池。常量池中的其餘類(接口)、方法、字段的符號引用也與此相似。
斷定一個常量是不是「廢棄常量」比較簡單,而要斷定一個類是不是「無用的類」的條件則相對苛刻許多。類須要同時知足下面3個條件才能算是「無用的類」。
- 該類全部的實例都已經被回收,也就是Java堆中不存在該類的任何實例。
- 加載該類的
ClassLoader
已經被回收。 - 該類對應的
java.lang.Class
對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。
新生代,老生代,永久代
在 Java 中,堆被劃分紅兩個不一樣的區域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被劃分爲三個區域:Eden、Survivor * 2。 **這樣劃分的目的是爲了使 JVM 可以更好的管理堆內存中的對象,包括內存的分配以及回收。**這種分法並無什麼新的思想,只是老年代用來放在好幾回GC中都活下來的新生代中的對象——能夠預期這些對象再以後的GC中應該也能存活。
在新生代中,新出的對象會在這裏,還不是久經考驗的對象會在這裏。這塊內存裏每次垃圾收集時都發現有大批對象死去,只有少許存活。於是能夠選用下述的「複製」算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「標記一清除」或者「標記一整理」算法來進行回收。
永久代是 HotSpot 虛擬機特有的概念,是方法區的一種實現,別的JVM都沒有這個東西。在 Java 8 中,永久代被完全移除,取而代之的是另外一塊與堆不相連的本地內存——元空間。 永久代或者「Perm Gen」包含了JVM須要的應用元數據,這些元數據描述了在應用裏使用的類和方法。注意,永久代不是Java堆內存的一部分。永久代存放JVM運行時使用的類。永久代一樣包含了Java SE庫的類和方法。永久代的對象在Full GC時進行垃圾收集。
收集算法
「標記一清除」( Mark-Sweep)算法
最基礎的收集算法是「標記一清除」( Mark-Sweep)算法,如同它的名字同樣,算法分爲「標記」和「清除」兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象,之因此說它是最基礎的收集算法,是由於後續的收集算法都是基於這種思路並對其不足進行改進而獲得的。它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另外一個是空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。
「複製」( Copying)算法
爲了解決效率問題,一種稱爲「複製」( Copying)的收集算法出現了,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲了原來的一半,未免過高了一點。
不過如今的商業虛擬機都採用這種收集算法的思想,只不過留存不用的比例有所調整。將新生代內存分爲一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。當回收時,將 Eden 和 Survivor 中還存活着的對象一次性地複製到另一塊 Survivor 空間上,最後清理掉 Eden 和剛纔用過的 Survivor 空間。HotSpot虛擬機上默認採用 Eden 和 Survivor 空間 8:1 的比例。不過,咱們沒有辦法保證每次回收都只有很少於10%的對象存活,當 Survivor空間不夠用時,須要依賴其餘內存(這裏指老年代)進行分配擔保( Handle Promotion)。
內存的分配擔保指:若是另一塊 Survivor 空間沒有足夠空間在放上一次新生代收集下來的存活對象時,這些對象將經過一些機制進入老年代。關於對新生代進行分配擔保的內容,在本章稍後在講解垃圾收集器執行規則時還會再詳細講解。
「標記-整理」( Mark-Compact)算法
複製收集算法在對象存活率較髙時就要進行較多的複製操做,效率將會變低。更關鍵的是,若是不想浪費太多空間, Survivor 空間必然較小,進而就須要有額外的空間進行分配擔保,以防沒法復活全部該復活的對象(試想所全部對象都要復活的極端狀況)。因此在老年代中不使用此算法。
根據老年代的特色,有人提出了另一種「標記-整理」( Mark-Compact)算法,標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對全部對象進行清理,而是讓全部繼續存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。
分代收集算法
就是在根據對象的生存能力的基礎上劃分新生代,老生代,永久代,在在此基礎上對不一樣」代「用不一樣收集算法。
性能考量
必須對GC算法的執行效率有嚴格的考量,才能保證虛擬機高效運行。
就可達性分析中從 GC Roots 節點找引用鏈這個操做來講,其中可做爲 GC Roots的節點主要在全局性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中,如今不少應用僅僅方法區就有數百兆,若是要逐個檢查這裏面的引用,那麼必然會消耗不少時間。
另外,可達性分析對執行時間的敏感還體如今GC停頓上,由於在整個分析期間整個JVM看起來就像被凍結在某個時間點上。這是由於若是在GC分析過程當中出現對象引用關係還在不斷變化的狀況,GC分析結果準確性就沒法獲得保證。因而GC進行時必須停頓全部Java執行線程(Sun將這件事情稱爲「 Stop The World」)。
GC roots 優化
Hotspot 使用 OopMap 避免對棧上徹底的遍歷。一個線程意味着一個棧,一個棧由多個棧幀組成,一個棧幀對應着一個方法,一個方法裏面可能有多個安全點。 gc 發生時,程序首先運行到最近的一個安全點停下來,而後更新本身的 OopMap ,記下棧上哪些位置表明着引用。枚舉根節點時,遞歸遍歷每一個棧幀的 OopMap ,經過棧中記錄的被引用對象的內存地址,便可找到這些對象( GC Roots )。
安全點與安全區域
在 OopMap的協助下, HotSpot能夠快速且準確地完成 GC Roots枚舉,但一個題隨之而來:線程中的信息可能/每每在不斷變化,進而其中包含許多引用關係變化,若是 OopMap 要響應這樣的變化,那麼會帶來糟糕的性能開銷。
所以, HotSpot 沒有實時地響應變化,只是」在特定的位置「記錄(更新)信息,這些位置稱爲安全點(safepoint)。因爲 GC 須要 OopMap,因此GC只有在到達安全點時才能進行是很天然的推斷。於是 Safepoint 的選定既不能太少以至於讓GC等待時間太長,也不能過於頻繁以至於過度增大運行時的負荷。
因此,安全點的選定基本上是以程序「是否具備讓程序長時間執行的特徵」爲標準進行選定的—由於每條指令執行的時間都很是短暫,程序不太可能由於指令流長度太長這個緣由而過長時間運行,長時間執行」的最明顯特徵就是指令序列複用,例如方法調用、循環跳轉、異常跳轉等,因此具備這些功能的指令纔會產生 Safepoint。
在 Java 這種以多線程併發爲亮點特性的語言中,多線程顯然是常見的運行場景。而多線程意味着各個線程同時到達安全點的可能性極低,因此是 GC 發出請求後,各個線程接收並要求本身停在安全點。這裏有兩種方案可供選擇:搶先式中斷( Preemptive Suspension)和主動式中斷( Voluntary Suspension),其中搶先式中斷不須要線程的執行代碼主動去配合,在GC發生時,首先把全部線程所有中斷,若是發現有線程中斷的地方不在安全點上,就恢復線程,讓它「跑」到安全點上。如今幾乎沒有虛擬機實現採用搶先式中斷來暫停線程從而響應GC事件。而主動式中斷的思想是當GC須要中斷線程的時候,不直接對線程操做,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌(輪詢標誌的地方和安全點是重合的),發現中斷標誌爲真時就本身中斷掛起。
如若線程處於 Sleep狀態或者 Blocked狀態,這時候線程沒法響應GC的要求,「走」到安全的地方去中斷掛起,GC也顯然不太可能等待線程從新被分配CPU時間。對於這種狀況,就須要安全區域( Safe Region)來解決。安全區域是指在一段代碼片斷之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的、咱們也能夠把 Safe Region看作是被擴展了的 Safepoint。在線程執行到 Safe Region中的代碼時,首先標識本身已經進入了 Safe Region,那樣,當在這段時間裏JVM要發起GC時,就不用管標識本身爲 Safe Region狀態的線程了。在線程要離開 Safe Region時,它要檢査系統是否已經完成了根節點枚舉(或者是整個GC過程),若是完成了,那線程就繼續執行,不然它就必須等待直到收到能夠安全離開 Safe Region的信號爲止。
垃圾收集器:
若是說收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。Java虛擬機規範中對垃圾收集器應該如何實現並無任何規定,所以不一樣的廠商、不一樣版本的虛擬機所提供的垃圾收集器均可能會有很大差異,而且通常都會提供參數供用戶根據本身的應用特色和要求組合出各個年代所使用的收集器。這裏討論的收集器基於JDK1.7 Update14以後的 HotSpot虛擬機(在這個版本中正式提供了商用的G1收集器,以前G1仍處於實驗狀態),這個虛擬機包含的全部收集器如圖3-5所示。圖3-5展現了7種做用於不一樣分代的收集器,若是兩個收集器之間存在連線,就說明它們能夠搭配使用。虛擬機所處的區域,則表示它是屬於新生代收集器仍是老年代收集器。接
Serial 收集器
Serial收集器是最基本、發展歷史最悠久的收集器,曾經(在JDK1.3.1以前)是虛擬機新生代收集的惟一選擇。如名字暗示的同樣,這個收集器是一個單線程的收集器,但它的「單線程」的意義並不只僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工做,更重要的是在它進行垃圾收集時,必須暫停其餘全部的工做線程,直到它收集結束。
「Stop The World」這個名字也許聽起來很酷,但這項工做其實是由虛擬機在後臺自動發起和自動完成的,在用戶不可見的狀況下把用戶正常工做的線程所有停掉,這對不少應用來講都是難以接受的。讀者不妨試想一下,要是你的計算機每運行一個小時就會暫停響應5分鐘,你會有什麼樣的心情?
Serial old是 Serial收集器的老年代版本,它一樣是一個單線程收集器。
ParNew 收集器
serial 收集器的多線程版本,只是在工做時本身多線程,其它線程仍是要停,即」Stop the world「沒有避免。
注意:從 ParDew收集器開始,後面還會接觸到幾款併發和並行的收集器。在你們可能產生疑惑以前,有必要先解釋兩個名詞:併發和並行。這兩個名詞都是併發編程中的概念,在談論垃圾收集器的上下文語境中,它們能夠解釋以下。
- 並行(Parallel):指多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態。
- 併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不必定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行於另外一個CPU上。
Paralle Scavenge 收集器
Parallel Scavenge收集器的特色是它的關注點與其餘收集器不一樣,CMS等收集器的關注點是儘量地縮短垃圾收集時用戶線程的停頓時間,而 Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量( Throughput)。所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即」吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)「,虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
停頓時間越短就越適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗,而高吞吐量則能夠高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務。
Parallel Old 是 Parallel Scavenge收集器的老年代版本,使用多線程和「標記一整理」算法。這個收集器是在JDK1.6中才開始提供的。
CMS 收集器
CMS ( Concurrent Mark Swee)收集器是一種以獲取最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤爲重視服務的響應速度,但願系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就很是符合這類應用的需求。
從名字(包含「 Mark Sweep」)上就能夠看出,CMs收集器是基於「標記清除」算法實現的,它的運做過程相對於前面幾種收集器來講更復雜一些,整個過程分爲4個步驟,包括:
- 初始標記( EMS initial mark)
- 併發標記( CMS concurrent mark)
- 從新標記( CMS remark)
- 併發清除( CMS concurrent sweep)
其中,初始標記、從新標記這兩個步驟仍然須要「 Stop The World」。初始標記僅僅只是標記一下 GC Roots能直接關聯到的對象,速度很快,併發標記階段就是在初始標記階段的基礎上完善引用鏈的過程。而從新標記階段則是爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但遠比並發標記的時間短。
因爲整個過程當中耗時最長的併發標記和併發清除過程收集器線程均可以與用戶線程一塊兒工做,因此,從整體上來講,CMS收集器的內存回收過程是與用戶線程一塊兒併發執行的。經過圖3-10能夠比較清楚地看到CMS收集器的運做步驟中併發和須要停頓的時間。
CMS存在以下缺點:
- CMS收集器對CPU資源很是敏感。其實,面向併發設計的程序都對CPU資源比較敏感。在併發階段,它雖然不會致使用戶線程停頓,可是會由於佔用了一部分線程(或者說CPU資源)而致使應用程序變慢,總吞吐量會下降。
- 因爲CMS併發清理階段用戶線程還在運行着,伴隨程序運行天然就還會有新的垃圾不斷產生,這一部分垃圾出如今標記過程以後,CMS沒法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱爲「浮動垃圾」。
- 也是因爲在垃圾收集階段用戶線程還須要運行,那也就還須要預留有足夠的內存空間給用戶線程使用,所以CMS收集器不能像其餘收集器那樣等到老年代幾乎徹底被填滿了再進行收集,須要預留一部分空間提供併發收集時的程序運做使用。在JDK15的默認設置下,CMS收集器當老年代使用了68%。能夠適當調高參數
-XX:CMSInitiatingOccupancyFraction
的值來提升觸發百分比。要是CMS運行期間預留的內存沒法知足程序須要,就會出現一次Concurrent Mode Failure
失敗,這時虛擬機將啓動後備預案:臨時啓用 Serial old收集器來從新進行老年代的垃圾收集,這樣停頓時間就很長了。因此說參數-XX:CMSInitiatingOccupancy Fraction
設置得過高很容易致使大量Concurrent Mode Failure
失敗,性能反而下降。 - 還有最後一個缺點,因爲CMS是一款基於「標記一清除」算法實現的收集器,這意味着收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,每每會出現老年代還有很大空間剩餘,可是沒法找到足夠大的連續空間來分配當前對象,不得不提早觸發一次 Full GC。
G1 收集器
Gl( Garbage- First)收集器是當今收集器技術發展的最前沿成果之一。
優勢:
- 並行與併發:G1能充分利用多CPU、多核環境下的硬件優點,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World 停頓的時間。
- G1收集器運做期間不會產生內存空間碎片。與CMS的「標記一清理」算法不一樣,G1從總體來看是基於「標記一整理」算法實現的收集器,從局部(兩個 Region之間)上來看是基於「複製」算法實現的,但不管如何,這兩種算法都意味着G1運做期間不會產生內存空間碎片,收集後能提供規整的可用內存。
- 這是G1相對於CMS的一大優點,下降停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過N毫秒。
在G1以前的其餘收集器進行收集的範圍都是整個新生代或者老年代,而G1再也不是這樣。使用G1收集器時,Java堆的內存佈局就與其餘收集器有很大差異,它將整個Java堆劃分爲多個大小相等的獨立區域( Region),雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔離的了,它們都是一部分 Region(不須要連續)的集合。
Gl收集器之因此能創建可預測的停頓時間模型,是由於它能夠有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個 Region 裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的 Region(這也就是 Garbage-First 名稱的來由)。這種使用Region劃份內存空間以及有優先級的區域回收方式,保證子G1收集器在有限的時間內能夠獲取儘量高的收集效率。
在G1收集器中, Region 之間的對象引用以及其餘收集器中的新生代與老年代之間的對象引用,虛擬機都是使用 Remembered Set 來避免全堆掃描的。G1收集器下的堆中每一個 Region 都有一個與之對應的 Remembered Set,虛擬機發現程序在對 Reference 類型的數據進行寫操做時,會產生一個 Write Barrier 暫時中斷寫操做,檢查 Reference引用的對象是否處於不一樣的 Region之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),若是是,便經過 CardTable 把相關引用信息記錄到被引用對象所屬的 Region的 Remembered Set之中。當進行內存回收時,在GC根節點的枚舉範圍中加入 Remembered Set 便可保證不對全堆掃描也不會有遺漏。
若是不計算維護 Remembered Set的操做,Gl收集器的運做大體可劃分爲如下幾個步驟
- 初始標記( Initial Marking )
- 併發標記( Concurrent Marking)
- 最終標記( Final Marking)
- 篩選回收( Live Data Counting and Evacuation)
G1收集器的前幾個步驟的運做過程和CMS收集器有不少類似之處。
初始標記階段僅僅只是標記一下 GC Roots 能直接關聯到的對象,而且修改TAMS( Next Top at Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確可用的 Region 中建立新對象,這階段須要停頓線程,但耗時很短。
併發標記階段是從 GC Root 開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序併發執行。
最終標記階段則是爲了修正在併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程 Remembered Set Logs 裏面,最終標記階段須要把 Remembered Set Logs 的數據合併到 Remembered Set中,這階段須要停頓線程,可是可要求並行執行。
最後在篩選回收階段首先對各個 Region的回收價值和成本進行排序,根據用戶所指望的GC停頓時間來制定回收計劃,從Sun公司透露出來的信息來看,這個階段其實也能夠作到與用戶程序一塊兒併發執行,可是由於只回收一部分 Region,時間是用戶可控制的,並且停頓用戶線程將大幅提升收集效率。
線程終結不致使對應實例的GC
當線程處於TERMINATED
狀態時,並不會致使該線程對應的實例被GC。可使用以下代碼進行嘗試:
import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; public class Test extends Thread { public Map<StringBuilder, StringBuilder> map = new HashMap<>(); public StringBuilder key = new StringBuilder("key"); @Override public synchronized void start() { map.put(key,new StringBuilder("hello")); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } super.start(); } }
首先是一個用來測試的線程類。
import java.lang.ref.WeakReference; import java.util.concurrent.TimeUnit; public class Main { public static void main(String[] args) throws InterruptedException { Test t = new Test(); t.start(); WeakReference<Thread> r = new WeakReference<>(t); WeakReference<StringBuilder> s = new WeakReference<>(t.map.get(t.key)); System.gc(); TimeUnit.SECONDS.sleep(3); System.out.println(t != null ? t.getState().toString() : "null"); System.out.println(s.get() != null ? s.get().toString() : "null"); t = null; System.gc(); TimeUnit.SECONDS.sleep(3); System.out.println(r.get() != null ? r.get().getState().toString() : "null"); System.out.println(s.get() != null ? s.get().toString() : "null"); } }
而後是正式測試,輸出結果爲:
TERMINATED hello null null
其中前兩行說明了,即使線程已經處於終結狀態,但其對應的實例並不會被GC,進而實例內部的成員變量所保持的強引用的對象不會被GC,進而這些對象內部的……總之就是無限套娃下去。