Java與C++之間有一堵由內存動態分配和垃圾收集技術所圍成的"高牆",牆外面的人想進去,牆裏面的人卻想出來。算法
概述
提及垃圾收集(Garbage Collection,GC),大部分人都把這項技術看成Java語言的伴生產物。事實上,GC的歷史比Java久遠,1960年誕生於MIT的Lisp是第一門真正使用內存動態分配和垃圾收集技術的語言。當Lisp還在胚胎時期時,人們就在思考GC須要完成的3件事情:安全
哪些內存須要回收?微信
何時回收?數據結構
如何回收?多線程
通過半個多世紀的發展,目前內存的動態分配與內存回收技術已經至關成熟,一切看起來都進入了"自動化"時代,那爲何咱們還要去了解GC和內存分配?答案很簡單:當須要排查各類內存溢出、內存泄漏問題時,當垃圾收集成爲系統達到更高併發量的瓶頸時,咱們就須要對這些"自動化"的技術實施必要的監控和調節。併發
對象已死嗎
垃圾收集器在作垃圾回收的時候,首先須要斷定的就是哪些內存是須要被回收的,哪些對象是「存活」的,是不能夠被回收的;哪些對象已經「死掉」了,須要被回收。高併發
引用計數法
Java
堆 中每一個具體對象(不是引用)都有一個引用計數器。當一個對象被建立並初始化賦值後,該變量計數設置爲1
。每當有一個地方引用它時,計數器值就加1。當引用失效時,即一個對象的某個引用超過了生命週期(出做用域後)或者被設置爲一個新值時,計數器值就減1。任何引用計數爲0
的對象能夠被看成垃圾收集。當一個對象被垃圾收集時,它引用的任何對象計數減1。佈局
優勢:性能
引用計數收集器執行簡單,斷定效率高,交織在程序運行中。對程序不被長時間打斷的實時環境比較有利。優化
缺點:
難以檢測出對象之間的循環引用。同時,引用計數器增長了程序執行的開銷。因此Java語言並無選擇這種算法進行垃圾回收。
可達性分析算法
可達性分析算法又叫根搜索算法,該算法的基本思想就是經過一系列稱爲「GC Roots」的對象做爲起始點,從這些起始點開始往下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到 GC Roots 對象之間沒有任何引用鏈的時候(不可達),證實該對象是不可用的,因而就會被斷定爲可回收對象。
以下圖所示: Object5
、Object6
、Object7
雖然互有關聯, 但它們到GC Roots
是不可達的, 所以也會被斷定爲可回收的對象。
在 Java 中可做爲 GC Roots 的對象包含如下幾種:
虛擬機棧(棧幀中的本地變量表)中引用的對象;
方法區中類靜態屬性引用的對象;
方法區中常量引用的對象;
本地方法棧中 JNI(Native 方法)引用的對象。
JVM中用到的全部現代GC算法在回收前都會先找出全部仍存活的對象。可達性分析算法是從離散數學中的圖論引入的,程序把全部的引用關係看做一張圖。下圖展現的JVM中的內存佈局能夠用來很好地闡釋這一律念:
再談引用
不管是經過引用計數器仍是經過可達性分析來判斷對象是否能夠被回收都設計到「引用」的概念。在 Java 中,根據引用關係的強弱不同,將引用類型劃爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)。
強引用:Object obj = new Object()
這種方式就是強引用,只要這種強引用存在,垃圾收集器就永遠不會回收被引用的對象。
軟引用:用來描述一些有用但非必須的對象。在 OOM 以前垃圾收集器會把這些被軟引用的對象列入回收範圍進行二次回收。若是本次回收以後仍是內存不足纔會觸發 OOM。在 Java 中使用 SoftReference 類來實現軟引用。
弱引用:同軟引用同樣也是用來描述非必須對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在 Java 中使用 WeakReference 類來實現。
虛引用:是最弱的一種引用關係,一個對象是否有虛引用的存在徹底不影響對象的生存時間,也沒法經過虛引用來獲取一個對象的實例。一個對象使用虛引用的惟一目的是爲了在被垃圾收集器回收時收到一個系統通知。在 Java 中使用 PhantomReference 類來實現。
生存仍是死亡
一個對象是否應該在垃圾回收器在GC
時回收,至少要經歷兩次標記過程。
第一次標記:若是對象在進行可達性分析後被斷定爲不可達對象,那麼它將被第一次標記而且進行一次篩選。篩選的條件是此對象是否有必要執行 finalize() 方法。對象沒有覆蓋 finalize() 方法或者該對象的 finalize() 方法曾經被虛擬機調用過,則斷定爲不必執行。
finalize()第二次標記:若是被斷定爲有必要執行 finalize() 方法,那麼這個對象會被放置到一個 F-Queue 隊列中,並在稍後由虛擬機自動建立的、低優先級的 Finalizer 線程去執行該對象的 finalize() 方法。可是虛擬機並不承諾會等待該方法結束,這樣作是由於,若是一個對象的 finalize() 方法比較耗時或者發生了死循環,就可能致使 F-Queue 隊列中的其餘對象永遠處於等待狀態,甚至致使整個內存回收系統崩潰。finalize() 方法是對象逃脫死亡命運的最後一次機會,若是對象要在 finalize() 中挽救本身,只要從新與 GC Roots 引用鏈關聯上就能夠了。這樣在第二次標記時它將被移除「即將回收」的集合,若是對象在這個時候尚未逃脫,那麼它基本上就真的被回收了。
回收方法區
前面介紹過,方法區在 HotSpot 虛擬機中被劃分爲永久代。在 Java 虛擬機規範中沒有要求方法區實現垃圾收集,並且方法區垃圾收集的性價比也很低。
方法區(永久代)的垃圾收集主要回收兩部份內容:廢棄常量和無用的類。
廢棄常量的回收和 Java 堆中對象的回收很是相似,這裏就不作過多的解釋了。
類的回收條件就比較苛刻了。要斷定一個類是否能夠被回收,要知足如下三個條件:
該類的全部實例已經被回收;
加載該類的 ClassLoader 已經被回收;
該類的 Class 對象沒有被引用,沒法再任何地方經過反射訪問該類的方法。
垃圾收集算法
標記-清除算法
標記-清除算法(Mark-Sweep)是一種常見的基礎垃圾收集算法,它將垃圾收集分爲兩個階段:
標記階段:標記出能夠回收的對象。
清除階段:回收被標記的對象所佔用的空間。
標記-清除算法之因此是基礎的,是由於後面講到的垃圾收集算法都是在此算法的基礎上進行改進的。
優勢:實現簡單,不須要對象進行移動。
缺點:標記、清除過程效率低,產生大量不連續的內存碎片,提升了垃圾回收的頻率。
標記-清除算法的執行的過程以下圖所示
複製算法
爲了解決標記-清除算法的效率不高的問題,產生了複製算法。它把內存空間劃爲兩個相等的區域,每次只使用其中一個區域。垃圾收集時,遍歷當前使用的區域,把存活對象複製到另一個區域中,最後將當前使用的區域的可回收的對象進行回收。
優勢:按順序分配內存便可,實現簡單、運行高效,不用考慮內存碎片。
缺點:可用的內存大小縮小爲原來的一半,對象存活率高時會頻繁進行復制。
複製算法的執行過程以下圖所示
如今的商業虛擬機都採用這種算法來回收新生代,在 IBM 的研究中新生代中的對象 98% 都是「朝生夕死」,因此並不須要按照 1:1 的比例來劃分空間,而是將內存分爲一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。當回收時,將 Eden 和 Survivor 中還存活的對象一次性複製到另外一塊 Survivor 空間上,最後清理掉 Eden 和剛纔用過的 Survivor 空間。HotSpot 默認 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用的內存爲整個新生代容量的 90%(80%+10%),只有 10% 會被浪費。固然,98% 的對象可回收只是通常場景下的數據,咱們沒辦法保證每次回收後都只有很少於 10% 的對象存活,當 Survivor 空間不夠用時,須要依賴其它內存(這裏指老年代)進行分配擔保。若是另一塊 Survivor 空間沒有足夠空間存放上一次新生代收集下來存活的對象時,這些對象將直接經過分配擔保機制進入老年代。
標記-整理算法
在新生代中可使用複製算法,可是在老年代就不能選擇複製算法了,由於老年代的對象存活率會較高,這樣會有較多的複製操做,致使效率變低。標記-清除算法能夠應用在老年代中,可是它效率不高,在內存回收後容易產生大量內存碎片。所以就出現了一種標記-整理算法(Mark-Compact)算法,與標記-整理算法不一樣的是,在標記可回收的對象後將全部存活的對象壓縮到內存的一端,使他們緊湊的排列在一塊兒,而後對端邊界之外的內存進行回收。回收後,已用和未用的內存都各自一邊。
優勢:解決了標記-清理算法存在的內存碎片問題。
缺點:仍須要進行局部對象移動,必定程度上下降了效率。
標記-整理算法的執行過程以下圖所示
分代收集算法
當前商業虛擬機都採用分代收集的垃圾收集算法。分代收集算法,顧名思義是根據對象的存活週期將內存劃分爲幾塊。通常包括年輕代、老年代 和 永久代,如圖所示:
新生代(Young generation)
絕大多數最新被建立的對象會被分配到這裏,因爲大部分對象在建立後會很快變得不可達,因此不少對象被建立在新生代,而後消失。對象從這個區域消失的過程咱們稱之爲 minor GC
。
新生代 中存在一個Eden
區和兩個Survivor
區。新對象會首先分配在Eden
中(若是新對象過大,會直接分配在老年代中)。在GC
中,Eden
中的對象會被移動到Survivor
中,直至對象知足必定的年紀(定義爲熬過GC
的次數),會被移動到老年代。
能夠設置新生代和老年代的相對大小。這種方式的優勢是新生代大小會隨着整個堆大小動態擴展。參數 -XX:NewRatio
設置老年代與新生代的比例。例如 -XX:NewRatio=8
指定 老年代/新生代 爲8/1
. 老年代 佔堆大小的 7/8
,新生代 佔堆大小的 1/8
(默認便是 1/8
)。
例如:
-XX:NewSize=64m -XX:MaxNewSize=1024m -XX:NewRatio=8
老年代(Old generation)
對象沒有變得不可達,而且重新生代中存活下來,會被拷貝到這裏。其所佔用的空間要比新生代多。也正因爲其相對較大的空間,發生在老年代上的GC
要比新生代要少得多。對象從老年代中消失的過程,能夠稱之爲major GC
(或者full GC
)。
永久代(permanent generation)
像一些類的層級信息,方法數據 和方法信息(如字節碼,棧 和 變量大小),運行時常量池(JDK7
以後移出永久代),已肯定的符號引用和虛方法表等等。它們幾乎都是靜態的而且不多被卸載和回收,在JDK8
以前的HotSpot
虛擬機中,類的這些**「永久的」** 數據存放在一個叫作永久代的區域。
永久代一段連續的內存空間,咱們在JVM
啓動以前能夠經過設置-XX:MaxPermSize
的值來控制永久代的大小。可是JDK8
以後取消了永久代,這些元數據被移到了一個與堆不相連的稱爲元空間 (Metaspace
) 的本地內存區域。
小結
當執行一次Minor Collection時,Eden空間的存活對象會被複制到To Survivor空間,而且以前通過一次Minor Collection並在From Survivor空間存活的仍年輕的對象也會複製到To Survivor空間。
有兩種狀況Eden空間和From Survivor空間存活的對象不會複製到To Survivor空間,而是晉升到老年代。一種是存活的對象的分代年齡超過-XX:MaxTenuringThreshold(用於控制對象經歷多少次Minor GC才晉升到老年代)所指定的閾值。另外一種是To Survivor空間容量達到閾值。
當全部存活的對象被複制到To Survivor空間,或者晉升到老年代,也就意味着Eden空間和From Survivor空間剩下的都是可回收對象,以下圖所示。
這時GC執行Minor Collection,Eden空間和From Survivor空間都會被清空,而存活的對象都存放在To Survivor空間。
接下來將From Survivor空間和To Survivor空間互換位置,也就是此前的From Survivor空間成爲了如今的To Survivor空間,每次Survivor空間互換都要保證To Survivor空間是空的,這就是複製算法在新生代中的應用。在老年代則採用了標記-壓縮算法。
JDK8
堆內存通常是劃分爲年輕代和老年代,不一樣年代 根據自身特性採用不一樣的垃圾收集算法。
對於新生代,每次GC
時都有大量的對象死亡,只有少許對象存活。考慮到複製成本低,適合採用複製算法。所以有了From Survivor
和To Survivor
區域。
對於老年代,由於對象存活率高,沒有額外的內存空間對它進行擔保。於是適合採用標記-清理算法和標記-整理算法進行回收。
因爲對象進行了分代處理,所以垃圾回收區域、時間也不同。垃圾回收有兩種類型,Minor GC 和 Full GC。
Minor GC:新生代垃圾收集。對新生代進行回收,不會影響到年老代。由於新生代的 Java 對象大多死亡頻繁,因此 Minor GC 很是頻繁,通常在這裏使用速度快、效率高的算法,使垃圾回收能儘快完成。
Full GC:也叫 Major GC,對整個堆進行回收,包括新生代和老年代(JDK8 取消永久代)。因爲Full GC須要對整個堆進行回收,因此比Minor GC要慢,所以應該儘量減小Full GC的次數。它的收集頻率較低,耗時較長。
垃圾收集算法小結
HotSpot的算法實現
枚舉根節點
可達性分析枚舉GC Roots時 ,必須stop the world
目前JVM使用準確式GC,停頓時並不須要一個個檢查,而是從預先存放的地方直接取。(HotSpot保存在OopMap數據結構中)
安全點
基於效率考慮,生成OopMap只會才特定的地方,稱爲安全點
安全點的選定方法
搶先式中斷:現代JVM不採用
主動式中斷:線程輪詢安全點標識,而後掛起
安全區域
對於沒有分配cpu的線程(sleep),安全點沒法處理,由安全區域解決
安全區域指一段代碼中引用關係不會發生變化
線程進入安全區域時,JVM發起GC就不用管這些線程,離開時須要檢查GC是否完成,未完成就須要等待
垃圾收集器
若是說垃圾收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。下圖展現了7種做用於不一樣分代的收集器,其中用於回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,還有用於回收整個Java堆的G1收集器。不一樣收集器之間的連線表示它們能夠搭配使用。
Serial收集器(複製算法): 新生代單線程收集器,標記和清理都是單線程,優勢是簡單高效;
ParNew收集器 (複製算法): 新生代收並行集器,其實是Serial收集器的多線程版本,在多核CPU環境下有着比Serial更好的表現;
Parallel Scavenge收集器 (複製算法): 新生代並行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用戶線程時間/(用戶線程時間+GC線程時間),高吞吐量能夠高效率的利用CPU時間,儘快完成程序的運算任務,適合後臺應用等對交互相應要求不高的場景;
Serial Old收集器 (標記-整理算法): 老年代單線程收集器,Serial收集器的老年代版本;
Parallel Old收集器 (標記-整理算法):老年代並行收集器,吞吐量優先,Parallel Scavenge收集器的老年代版本;
CMS(Concurrent Mark Sweep)收集器(標記-清除算法):老年代並行收集器,以獲取最短回收停頓時間爲目標的收集器,具備高併發、低停頓的特色,追求最短GC回收停頓時間。
G1(Garbage First)收集器 (標記-整理算法):Java堆並行收集器,G1收集器是JDK1.7提供的一個新收集器,G1收集器基於「標記-整理」算法實現,也就是說不會產生內存碎片。此外,G1收集器不一樣於以前的收集器的一個重要特色是:G1回收的範圍是整個Java堆(包括新生代,老年代),而前六種收集器回收的範圍僅限於新生代或老年代。
附加:如下爲垃圾回收器的詳細說明,能夠選擇性閱讀
垃圾回收器分類標準
七種垃圾回收器概述
在 JVM
中,具體實現有 Serial
、ParNew
、Parallel Scavenge
、CMS
、Serial Old(MSC)
、Parallel Old
、G1
等。在下圖中,你能夠看到 不一樣垃圾回收器 適合於 不一樣的內存區域,若是兩個垃圾回收器之間 存在連線,那麼表示二者能夠 配合使用。
若是當 垃圾回收器 進行垃圾清理時,必須 暫停 其餘全部的 工做線程,直到它徹底收集結束。咱們稱這種須要暫停工做線程才能進行清理的策略爲 Stop-the-World
。以上回收器中, Serial
、ParNew
、Parallel Scavenge
、Serial Old
、Parallel Old
均採用的是 Stop-the-World
的策略。
圖中有 7
種不一樣的 垃圾回收器,它們分別用於不一樣分代的垃圾回收。
新生代回收器:Serial、ParNew、Parallel Scavenge
老年代回收器:Serial Old、Parallel Old、CMS
整堆回收器:G1
兩個 垃圾回收器 之間有連線表示它們能夠 搭配使用,可選的搭配方案以下:
新生代 | 老年代 |
---|---|
Serial | Serial Old |
Serial | CMS |
ParNew | Serial Old |
ParNew | CMS |
Parallel Scavenge | Serial Old |
Parallel Scavenge | Parallel Old |
G1 | G1 |
單線程垃圾回收器
Serial(-XX:+UseSerialGC)
Serial
回收器是最基本的 新生代 垃圾回收器,是 單線程 的垃圾回收器。因爲垃圾清理時,Serial
回收器 不存在 線程間的切換,所以,特別是在單 CPU
的環境下,它的 垃圾清除效率 比較高。對於 Client
運行模式的程序,選擇 Serial
回收器是一個不錯的選擇。
Serial
新生代回收器 採用的是 複製算法。
Serial Old(-XX:+UseSerialGC)
Serial Old
回收器是 Serial
回收器的 老生代版本,屬於 單線程回收器,它使用 標記-整理 算法。對於 Server
模式下的虛擬機,在 JDK1.5
及其之前,它常與 Parallel Scavenge
回收器配合使用,達到較好的 吞吐量,另外它也是 CMS
回收器在 Concurrent Mode Failure
時的 後備方案。
Serial
回收器和 Serial Old
回收器的執行效果以下:
Serial Old
老年代回收器 採用的是 標記 - 整理算法。
多線程垃圾回收器(吞吐量優先)
ParNew(-XX:+UseParNewGC)
ParNew
回收器是在 Serial
回收器的基礎上演化而來的,屬於 Serial
回收器的 多線程版本,一樣運行在 新生代區域。在實現上,二者共用不少代碼。在不一樣運行環境下,根據 CPU
核數,開啓 不一樣的線程數,從而達到 最優 的垃圾回收效果。對於那些 Server
模式的應用程序,若是考慮採用 CMS
做爲 老生代回收器 時,ParNew
回收器是一個不錯的選擇。
ParNew
新生代回收器 採用的是 複製算法。
Parallel Scavenge(-XX:+UseParallelGC)
和 ParNew
回收同樣,Parallel Scavenge
回收器也是運行在 新生代區域,屬於 多線程 的回收器。但不一樣的是,ParNew
回收器是經過控制 垃圾回收 的 線程數 來進行參數調整,而 Parallel Scavenge
回收器更關心的是 程序運行的吞吐量。即一段時間內,用戶代碼 運行時間佔 總運行時間 的百分比。
Parallel Scavenge
新生代回收器 採用的是 複製算法。
Parallel Old(-XX:+UseParallelOldGC)
Parallel Old
回收器是 Parallel Scavenge
回收器的 老生代版本,屬於 多線程回收器,採用 標記-整理算法。Parallel Old
回收器和 Parallel Scavenge
回收器一樣考慮了 吞吐量優先 這一指標,很是適合那些 注重吞吐量 和 CPU
資源敏感 的場合。
Parallel Old
老年代回收器 採用的是 標記 - 整理算法。
其餘的回收器(停頓時間優先)
CMS(-XX:+UseConcMarkSweepGC)
CMS(Concurrent Mark Sweep)
回收器是在 最短回收停頓時間 爲前提的回收器,屬於 多線程回收器,採用 標記-清除算法。
相比以前的回收器,CMS
回收器的運做過程比較複雜,分爲四步:
初始標記(CMS initial mark)
初始標記 僅僅是標記 GC Roots
內 直接關聯 的對象。這個階段 速度很快,須要 Stop the World
。
併發標記(CMS concurrent mark)
併發標記 進行的是 GC Tracing
,從 GC Roots
開始對堆進行 可達性分析,找出 存活對象。
從新標記(CMS remark)
從新標記 階段爲了 修正 併發期間因爲 用戶進行運做 致使的 標記變更 的那一部分對象的 標記記錄。這個階段的 停頓時間 通常會比 初始標記階段 稍長一些,但遠比 併發標記 的時間短,也須要 Stop The World
。
併發清除(CMS concurrent sweep)
併發清除 階段會清除垃圾對象。
初始標記(CMS initial mark
)和 從新標記(CMS remark
)會致使 用戶線程 卡頓,Stop the World
現象發生。
在整個過程當中,CMS
回收器的 內存回收 基本上和 用戶線程 併發執行,以下所示:
因爲 CMS
回收器 併發收集、停頓低,所以有些地方成爲 併發低停頓回收器(Concurrent Low Pause Sweep Collector
)。
CMS
回收器的缺點:
CMS回收器對CPU資源很是依賴
CMS
回收器過度依賴於 多線程環境,默認狀況下,開啓的 線程數 爲(CPU 的數量 + 3)/ 4
,當 CPU
數量少於 4
個時,CMS
對 用戶查詢 的影響將會很大,由於他們要分出一半的運算能力去 執行回收器線程;
CMS回收器沒法清除浮動垃圾
因爲 CMS
回收器 清除已標記的垃圾 (處於最後一個階段)時,用戶線程 還在運行,所以會有新的垃圾產生。可是這部分垃圾 未被標記,在下一次 GC
才能清除,所以被成爲 浮動垃圾。
因爲 內存回收 和 用戶線程 是同時進行的,內存在被 回收 的同時,也在被 分配。當 老生代 中的內存使用超過必定的比例時,系統將會進行 垃圾回收;當 剩餘內存 不能知足程序運行要求時,系統將會出現 Concurrent Mode Failure
,臨時採用 Serial Old
算法進行 清除,此時的 性能 將會下降。
垃圾收集結束後殘餘大量空間碎片
CMS
回收器採用的 標記清除算法,自己存在垃圾收集結束後殘餘 大量空間碎片 的缺點。CMS
配合適當的 內存整理策略,在必定程度上能夠解決這個問題。
G1回收器(垃圾區域Region優先)
G1
是 JDK 1.7
中正式投入使用的用於取代 CMS
的 壓縮回收器。它雖然沒有在物理上隔斷 新生代 與 老生代,可是仍然屬於 分代垃圾回收器。G1
仍然會區分 年輕代 與 老年代,年輕代依然分有 Eden
區與 Survivor
區。
G1
首先將 堆 分爲 大小相等 的 Region
,避免 全區域 的垃圾回收。而後追蹤每一個 Region
垃圾 堆積的價值大小,在後臺維護一個 優先列表,根據容許的回收時間優先回收價值最大的 Region
。同時 G1
採用 Remembered Set
來存放 Region
之間的 對象引用 ,其餘回收器中的 新生代 與 老年代 之間的對象引用,從而避免 全堆掃描。G1
的分區示例以下圖所示:
這種使用 Region
劃分 內存空間 以及有 優先級 的區域回收方式,保證 G1
回收器在有限的時間內能夠得到儘量 高的回收效率。
G1
和 CMS
運做過程有不少類似之處,整個過程也分爲 4
個步驟:
1.初始標記(CMS initial mark)
初始標記 僅僅是標記 GC Roots
內 直接關聯 的對象。這個階段 速度很快,須要 Stop the World
。
2.併發標記(CMS concurrent mark)
併發標記 進行的是 GC Tracing
,從 GC Roots
開始對堆進行 可達性分析,找出 存活對象。
3.從新標記(CMS remark)
從新標記 階段爲了 修正 併發期間因爲 用戶進行運做 致使的 標記變更 的那一部分對象的 標記記錄。這個階段的 停頓時間 通常會比 初始標記階段 稍長一些,但遠比 併發標記 的時間短,也須要 Stop The World
。
4.篩選回收
首先對各個 Region
的 回收價值 和 成本 進行排序,根據用戶所指望的 GC
停頓時間 來制定回收計劃。這個階段能夠與用戶程序一塊兒 併發執行,可是由於只回收一部分 Region
,時間是用戶可控制的,並且停頓 用戶線程 將大幅提升回收效率。
與其它 GC
回收相比,G1
具有以下 4
個特色:
並行與併發
使用多個 CPU
來縮短 Stop-the-World
的 停頓時間,部分其餘回收器須要停頓 Java
線程執行的 GC
動做,G1
回收器仍然能夠經過 併發的方式 讓 Java
程序繼續執行。
分代回收
與其餘回收器同樣,分代概念 在 G1
中依然得以保留。雖然 G1
能夠不須要 其餘回收器配合 就能獨立管理 整個GC堆,但它可以採用 不一樣的策略 去處理 新建立的對象 和 已經存活 一段時間、熬過屢次 GC
的舊對象,以獲取更好的回收效果。新生代 和 老年代 再也不是 物理隔離,是多個 大小相等 的獨立 Region
。
空間整合
與 CMS
的 標記—清理 算法不一樣,G1
從 總體 來看是基於 標記—整理 算法實現的回收器。從 局部(兩個 Region
之間)上來看是基於 複製算法 實現的。
但不管如何,這 兩種算法 都意味着 G1
運做期間 不會產生內存空間碎片,回收後能提供規整的可用內存。這種特性有利於程序長時間運行,分配大對象 時不會由於沒法找到 連續內存空間 而提早觸發 下一次 GC
。
可預測的停頓
這是 G1
相對於 CMS
的另外一大優點,下降停頓時間 是 G1
和 CMS
共同的關注點。G1
除了追求 低停頓 外,還能創建 可預測 的 停頓時間模型,能讓使用者明確指定在一個 長度 爲 M
毫秒的 時間片斷 內,消耗在 垃圾回收 上的時間不得超過 N
毫秒。(後臺維護的 優先列表,優先回收 價值大 的 Region
)。
內存分配與回收策略
所謂自動內存管理,最終要解決的也就是內存分配和內存回收兩個問題。前面咱們介紹了內存回收,這裏咱們再來聊聊內存分配。
對象的內存分配一般是在 Java 堆上分配(隨着虛擬機優化技術的誕生,某些場景下也會在棧上分配,後面會詳細介紹),對象主要分配在新生代的 Eden 區,若是啓動了本地線程緩衝,將按照線程優先在 TLAB 上分配。少數狀況下也會直接在老年代上分配。總的來講分配規則不是百分百固定的,其細節取決於哪種垃圾收集器組合以及虛擬機相關參數有關,可是虛擬機對於內存的分配仍是會遵循如下幾種「普世」規則:
對象優先在 Eden 區分配
多數狀況,對象都在新生代 Eden 區分配。當 Eden 區分配沒有足夠的空間進行分配時,虛擬機將會發起一次 Minor GC。若是本次 GC 後仍是沒有足夠的空間,則將啓用分配擔保機制在老年代中分配內存。
這裏咱們提到 Minor GC,若是你仔細觀察過 GC 平常,一般咱們還能從日誌中發現 Major GC/Full GC。
Minor GC 是指發生在新生代的 GC,由於 Java 對象大多都是朝生夕死,全部 Minor GC 很是頻繁,通常回收速度也很是快;
Major GC/Full GC 是指發生在老年代的 GC,出現了 Major GC 一般會伴隨至少一次 Minor GC。Major GC 的速度一般會比 Minor GC 慢 10 倍以上。
大對象直接進入老年代
所謂大對象是指須要大量連續內存空間的對象,頻繁出現大對象是致命的,會致使在內存還有很多空間的狀況下提早觸發 GC 以獲取足夠的連續空間來安置新對象。
前面咱們介紹過新生代使用的是標記-清除算法來處理垃圾回收的,若是大對象直接在新生代分配就會致使 Eden 區和兩個 Survivor 區之間發生大量的內存複製。所以對於大對象都會直接在老年代進行分配。
長期存活對象將進入老年代
虛擬機採用分代收集的思想來管理內存,那麼內存回收時就必須判斷哪些對象應該放在新生代,哪些對象應該放在老年代。所以虛擬機給每一個對象定義了一個對象年齡的計數器,若是對象在 Eden 區出生,而且可以被 Survivor 容納,將被移動到 Survivor 空間中,這時設置對象年齡爲 1。對象在 Survivor 區中每「熬過」一次 Minor GC 年齡就加 1,當年齡達到必定程度(默認 15) 就會被晉升到老年代。
動態對象年齡斷定
爲了更好的適應不一樣程序的內存狀況,虛擬機並非永遠要求對象的年齡必需達到某個固定的值(好比前面說的 15)纔會被晉升到老年代,而是會去動態的判斷對象年齡。若是在 Survivor 區中相同年齡全部對象大小的總和大於 Survivor 空間的一半,年齡大於等於該年齡的對象就能夠直接進入老年代。
空間分配擔保
在新生代觸發 Minor GC 後,若是 Survivor 中任然有大量的對象存活就須要老年隊來進行分配擔保,讓 Survivor 區中沒法容納的對象直接進入到老年代。
本章小結
本章介紹了垃圾收集的算法,幾款JDK1.7中提供的垃圾收集器特色以及運做原理。經過代碼實例驗證了Java虛擬機中自動內存分配及回收的主要規則。
內存回收與垃圾收集器在不少時候都是影響系統性能,併發能力的主要因素之一,虛擬機之因此提供多種不一樣的收集器以及提供大量的調節參數,是由於只有根據實際應用需求、實現方式選擇最優的收集方式才能獲取最高的性能。
原文連接:
https://thinkwon.blog.csdn.net/article/details/103831676
本文分享自微信公衆號 - 源代碼社區(ydmsq666)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。