1. 堆(Java堆) 堆是java虛擬機所管理的內存中最大的一塊內存區域,也是被各個線程共享的內存區域,
在JVM啓動時建立,該內存區域存放了對象實例(包括基本類型的變量及其值)及數組(全部new的對象)。
可是並非全部的對象都在堆上,因爲棧上分配和標量替換,致使有些對象不在堆上。 其大小經過-Xms(最小值)和-Xmx(最大值)參數設置,
1. -Xms爲JVM啓動時申請的最小內存,默認爲操做系統物理內存的1/64但小於1G,
2. -Xmx爲JVM可申請的最大內存,默認爲物理內存的1/4但小於1G,
3. 默認當空餘堆內存小於40%時,JVM會增大Heap到-Xmx指定的大小,可經過-XX:MinHeapFreeRation 來指定這個比列;
4. 當空餘堆內存大於70%時,JVM會減少heap的大小到-Xms指定的大小,可經過XX:MaxHeapFreeRation來指定這個比例,
5. 對於運行系統,爲避免在運行時頻繁調整Heap的大小,一般-Xms與-Xmx的值設成同樣。 因爲如今收集器都是採用分代收集算法,堆被劃分爲新生代和老年代。
新生代主要存儲新建立的對象和還沒有進入老年代的對象。
老年代存儲通過屢次新生代GC(Minor GC)仍然存活的對象。 堆中沒有足夠的內存完成實例分配,而且堆也沒法擴展時,將會出現OOM異常。(內存泄漏 / 內存溢出)。知足下面兩個條件就會拋出OOM。 (1)JVM 98% 的時間都花費在內存回收。 (2)每次回收的內存小於2%。 同一對象在執行期間若已經存儲在集合中,則不能修改影響hashCode值的相關信息,不然會致使內存泄露問題。 1.1 爲何要分代 堆內存是虛擬機管理的內存中最大的一塊,也是垃圾回收最頻繁的一塊區域,咱們程序全部的對象實例都存放在堆內存中。
給堆內存分代是爲了提升對象內存分配和垃圾回收的效率。
試想一下,若是堆內存沒有區域劃分,全部的新建立的對象和生命週期很長的對象放在一塊兒,隨着程序的執行,堆內存須要頻繁進行垃圾收集,
而每次回收都要遍歷全部的對象,遍歷這些對象所花費的時間代價是巨大的,會嚴重影響咱們的GC效率,這簡直太可怕了。 有了內存分代,狀況就不一樣了,
1. 新建立的對象會在新生代中分配內存,
2. 通過屢次回收仍然存活下來的對象存放在老年代中,靜態屬性、類信息等存放在永久代中,
3. 新生代中的對象存活時間短,只須要在新生代區域中頻繁進行GC,
4. 老年代中對象生命週期長,內存回收的頻率相對較低,不須要頻繁進行回收,
5. 永久代中回收效果太差,通常不進行垃圾回收
還能夠根據不一樣年代的特色採用合適的垃圾收集算法。
分代收集大大提高了收集效率,這些都是內存分代帶來的好處。 1.2 新生代 程序新建立的對象都是重新生代分配內存,
新生代由Eden Space和兩塊相同大小的Survivor Space(一般又稱S0和S1或From和To)構成,默認比例爲8:1:1。
劃分的目的是由於HotSpot採用複製算法來回收新生代,設置這個比例是爲了充分利用內存空間,減小浪費。
新生成的對象在Eden區分配(大對象除外,大對象直接進入老年代),當Eden區沒有足夠的空間進行分配時,虛擬機將發起一次Minor GC。 1. GC開始時,對象只會存在於Eden區和From Survivor區,To Survivor區是空的(做爲保留區域)。
2. GC進行時,Eden區中全部存活的對象都會被複制到To Survivor區,
3. 而在From Survivor區中,仍存活的對象會根據它們的年齡值決定去向,年齡值達到年齡閥值的對象會被移到老年代中,沒有達到閥值的對象會被複制到To Survivor區。
(默認爲15,新生代中的對象每熬過一輪垃圾回收,年齡值就加1,GC分代年齡存儲在對象的header中)
4. 接着清空Eden區和From Survivor區,
5. 新生代中存活的對象都在To Survivor區。
6. 接着, From Survivor區和To Survivor區會交換它們的角色,也就是新的To Survivor區就是上次GC清空的From Survivor區,新的From Survivor區就是上次GC的To Survivor區,
總之,無論怎樣都會保證To Survivor區在一輪GC後是空的。
7. GC時當To Survivor區沒有足夠的空間存放上一次新生代收集下來的存活對象時,須要依賴老年代進行分配擔保,將這些對象存放在老年代中。
可經過-Xmn參數來指定新生代的大小,
也能夠經過-XX:SurvivorRation來調整Eden Space及Survivor Space的大小。 1.3 老年代 用於存放通過屢次新生代GC仍然存活的對象,老年代中的對象生命週期較長,存活率比較高,在老年代中進行GC的頻率相對而言較低,並且回收的速度也比較慢。
老年代所佔的內存大小爲-Xmx對應的值減去-Xmn對應的值。 主要存儲的有:如緩存對象,新建的對象也有可能直接進入老年代,
主要有兩種狀況: ①大對象,可經過啓動參數設置-XX:PretenureSizeThreshold=1024(單位爲字節,默認爲0)來表明超過多大時就不在新生代分配,而是直接在老年代分配。 ②大的數組對象,且數組中無引用外部對象。
1.4 Java8 內存分代的改進 在 JDK 1.8 中, HotSpot 已經沒有 「PermGen space」這個區域了,取而代之是一個叫作 Metaspace(元空間) 的東西。 實際上在JDK1.7中,存儲在永久代的部分數據就已經轉移到了Java Heap或者是 Native Heap。
但永久代仍存在於JDK1.7中,並沒徹底移除,
譬如符號引用(Symbols)轉移到了native heap;
字面量(interned strings)轉移到了java heap;
類的靜態變量(class statics)轉移到了java heap。 元空間的本質和永久代相似,都是對JVM規範中方法區的實現。
不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。
所以,默認狀況下,元空間的大小僅受本地內存限制,但能夠經過如下參數來指定元空間的大小: -XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:若是釋放了大量的空間,就適當下降該值;若是釋放了不多的空間,那麼在不超過MaxMetaspaceSize時,適當提升該值。 -XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。 -XX:MinMetaspaceFreeRatio,在GC以後,最小的Metaspace剩餘空間容量的百分比,減小爲分配空間所致使的垃圾收集。 -XX:MaxMetaspaceFreeRatio,在GC以後,最大的Metaspace剩餘空間容量的百分比,減小爲釋放空間所致使的垃圾收集。 取消永久代的緣由: (1)字符串存在永久代中,容易出現性能問題和內存溢出。 (2)類及方法的信息等比較難肯定其大小,所以對於永久代的大小指定比較困難,過小容易出現永久代溢出,太大則容易致使老年代溢出。 (3)永久代會爲 GC 帶來沒必要要的複雜度,而且回收效率偏低。
若是不進行垃圾回收,內存早晚都會被消耗空,由於咱們在不斷的分配內存空間而不進行回收。除非內存無限大,咱們能夠任性的分配而不回收,可是事實並不是如此。因此,垃圾回收是必須的。Java 中的垃圾回收通常是在 Java 堆中進行,由於堆中幾乎存放了 Java 中全部的對象實例。html
在java中,程序員是不須要顯示的去釋放一個對象的內存的,而是由虛擬機自行執行。在JVM中,有一個垃圾回收線程,它是低優先級的,在正常狀況下是不會執行的,只有在虛擬機空閒或者當前堆內存不足時,纔會觸發執行,掃面那些沒有被任何引用的對象,並將它們添加到要回收的集合中,進行回收。java
垃圾回收器負責:
分配內存
保證全部正在被引用的對象還存在於內存中
回收執行代碼已經再也不引用的對象所佔的內存
GC自己是會週期性的自動運行的,由JVM決定運行的時機,並且如今的版本有多種更智能的模式能夠選擇,還會根據運行的機器自動去作選擇,就算真的有性能上的需求,也應該去對GC的運行機制進行微調,而不是經過使用這個命令來實現性能的優化。程序員
每一個 Java 應用程序都有一個 Runtime 類實例,使應用程序可以與其運行的環境相鏈接。能夠經過 getRuntime 方法獲取當前運行。java.lang.System.gc()只是java.lang.Runtime.getRuntime().gc()的簡寫,二者的行爲沒有任何不一樣。惟一的區別就是System.gc()寫起來比Runtime.getRuntime().gc()簡單點. 其實基本沒什麼機會用獲得這個命令, 由於這個命令只是建議JVM安排GC運行, 還有可能徹底被拒絕。 面試
GC在後臺自動發起和自動完成的,在用戶不可見的狀況下,把用戶正常的工做線程所有停掉,即GC停頓,會帶給用戶不良的體驗;算法
爲何要Stop-The-World?
可達性分析的時候爲了確保快照的一致性,須要對整個系統進行凍結,不能夠出現分析過程當中對象引用關係還在不斷變化的狀況,也就是Stop-The-World。segmentfault
Stop-The-World是致使GC卡頓的重要緣由之一。數組
串行和並行都會致使STW,併發不會致使STW緩存
概念:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。安全
可是:主流的java虛擬機並無選用引用計數算法來管理內存,其中最主要的緣由是:它很難解決對象之間相互循環引用的問題。服務器
優勢:算法的實現簡單,斷定效率也高,大部分狀況下是一個不錯的算法。不少地方應用到它
缺點:引用和去引用伴隨加法和減法,影響性能
致命的缺陷:對於循環引用的對象沒法進行回收
概念:這個算法的基本思路就是經過一系列的稱謂「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索走過的路徑稱爲引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連時,則證實此對象是不可用的。
可達性分析:
在Java語言中,可做爲GC Roots的對象包括下面幾種:
1.虛擬機棧(棧幀中本地變量表)中引用的對象。
2.本地方法棧中JNI(即通常說的Native方法)引用的對象。
3.方法區中類靜態屬性引用的對象。
4.方法區中常量引用的對象。
在 JDK1.2 以後,Java 對引用的概念進行了擴充,將其分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,引用強度依次減弱。
強引用具有如下三個個特色:
如「Object obj = new Object()」,這類引用是 Java 程序中最廣泛的。只要強引用還存在,垃圾收集器就永遠不會回收掉被引用的對象。
用來描述一些還有用但並不是必須的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收。若是此次回收尚未足夠的內存,纔會拋出內存溢出異常。在 JDK 1.2 以後,提供了 SoftReference 類來實現軟引用。
軟引用能夠加速JVM對垃圾內存的回收速度,能夠維護系統的運行安全,防止內存溢出等問題。
對於軟引用關聯着的對象,只有在內存不足的時候JVM纔會回收該對象。所以,這一點能夠很好地用來解決OOM的問題,而且這個特性很適合用來實現緩存:好比網頁緩存、圖片緩存等
用來描述非必須的對象,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發送以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。一旦一個弱引用對象被垃圾回收器回收,便會加入到一個註冊引用隊列中。在 JDK 1.2 以後,提供了 WeakReference類來實現弱引用。
Tips:軟引用、弱引用都很是適合來保存那些無關緊要的緩存數據。若是這麼作,當系統內存不足時,這些緩存數據會被回收,不會致使內存溢出。而當內存資源充足時,這些緩存數據又能夠存在至關長的時間,從而起到加速系統的做用。
它是最弱的一種引用關係。一個持有虛引用的對象,和沒有引用幾乎是同樣的,隨時都有可能被垃圾回收器回收。當試圖經過虛引用的get()方法取得強引用時,老是會失敗。而且,虛引用必須和引用隊列一塊兒使用,它的做用在於跟蹤垃圾回收過程。在 JDK 1.2 以後,提供了 PhantomReference類來實現虛引用。
可達性分析算法中不可達的對象,並不是是「非死不可」的,這時候它們暫時處於「緩刑」階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:
一、對象在進行可達性分析後被發現不可達,它將會被第一次標記並進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法,當對象沒有覆蓋finalize()方法或者finalize()方法已經被JVM調用過,那麼就不必執行finalize()方法;若是被斷定爲有必要執行finalize()方法,那麼此對象將會放置在一個叫作F-Quenen的隊列之中,並在稍後由一個虛擬機自動創建的、低優先級的Finalize線程去觸發這個方法。
二、稍後GC將對F-Quenen中的對象進行第二次小規模的標記,若是對象要在finalize()中成功拯救本身——只要從新與引用鏈上的任何一個對象創建關係便可,譬如把本身(this關鍵字)賦值給某個類變量或者對象的成員變量,那麼在第二次標記時它將被移出「即將回收」集合;finalize()方法是對象逃脫死亡的最後一次機會,若是對象這時候尚未成功逃脫,那他就會真的被回收了。
若是不使用finalize()方法逃脫的話,二次標記後刪除。
常量未被引用
方法區主要回收的是無用的類,那麼如何判斷一個類是無用的類的呢?
類須要同時知足下面3個條件才能算是 「無用的類」 :
虛擬機能夠對知足上述3個條件的無用類進行回收,這裏說的僅僅是「能夠」,而並非和對象同樣不使用了就會必然被回收。
對象分配
對象優先在Eden區分配。當Eden區沒有足夠空間分配時, VM發起一次Minor GC, 將 Eden區和其中一塊Survivor區內尚存活的對象放入另外一塊Survivor區域。如Minor GC時survivor空間不夠,對象提早進入老年代,老年代空間不夠時進行Full GC;
大對象直接進入老年代,如字符串,數組等大量連續內存空間的對象,避免在Eden區和Survivor區之間產生大量的內存複製, 此 外大對象容易致使還有很多空閒內存就提早觸發GC以獲取足夠的連續空間.
對象晉級
年齡閾值(長期存活的對象將進入老年代):VM爲每一個對象定義了一個對象年齡(Age)計數器, 經第一次Minor GC後 仍然存活, 被移動到Survivor空間中, 並將年齡設爲1. 之後對象在Survivor區中每熬 過一次Minor GC年齡就+1. 當增長到必定程度(-XX:MaxTenuringThreshold, 默認 15), 將會晉升到老年代.
提早晉升(動態對象年齡斷定再分段): 動態年齡斷定;若是在Survivor空間中相同年齡全部對象大小的總和大 於Survivor空間的一半, 年齡大於或等於該年齡的對象就能夠直接進入老年代, 而無 須等到晉升年齡.
爲了提高內存分配效率,在年輕代的Eden區HotSpot虛擬機使用了兩種技術來加快內存分配 ,分別是bump-the-pointer和TLAB(Thread-Local Allocation Buffers)。 1. bump-the-pointer 因爲Eden區是連續的,所以bump-the-pointer技術的核心就是跟蹤最後建立的一個對象,在對象建立時,只須要檢查最後一個對象後面是否有足夠的內存便可,從而大大加快內存分配速度; 2. TLAB技術 而對於TLAB技術是對於多線程而言的, 它會爲每一個新建立的線程在新生代的Eden Space上分配一塊獨立的空間,這塊空間稱爲TLAB(Thread Local Allocation Buffer),
其大小由JVM根據運行狀況計算而得 在TLAB上分配內存不須要加鎖,通常JVM會優先在TLAB上分配內存,若是對象過大或者TLAB空間已經用完,則仍然在堆上進行分配。 所以,在編寫程序時,多個小對象比大的對象分配起來效率更高。
用- XX:TLABWasteTargetPercent來設置其可佔用的Eden Space的百分比,默認是1%。
用 -XX:+PrintTLAB來查看TLAB空間的使用狀況。
用 -XX:+UseAdaptiveSizePolicy開關來控制是否採用動態控制策略,若是動態控制,則動態調整Java堆中各個區域的大小以及進入老年代的年齡。
用 -XX:PretenureSizeThreshold來控制直接升入老年代的對象大小,大於這個值的對象會直接分配在老年代上。
在一開始的時候,JVM的GC就是採用標記-清除-壓縮方式進行的,這麼作並非很高效,由於當對象分配的愈來愈多時,對象列表也越來也大,掃描和移動愈來愈耗時,形成了內存回收愈來愈慢。然而,通過根據對java應用的分析,發現大部分對象的存活時間都很是短,只有少部分數據存活週期是比較長的,請看下面對java對象內存存活時間的統計:
從圖表中能夠看出,大部分對象存活時間是很是短的,隨着時間的推移,被分配的對象愈來愈少。
1.1 爲何要分代 1. 給堆內存分代是爲了提升對象內存分配和垃圾回收的效率。 2. 還能夠根據不一樣年代的特色採用合適的垃圾收集算法。
堆內存是虛擬機管理的內存中最大的一塊,也是垃圾回收最頻繁的一塊區域,咱們程序全部的對象實例都存放在堆內存中。 試想一下,若是堆內存沒有區域劃分,全部的新建立的對象和生命週期很長的對象放在一塊兒,隨着程序的執行,堆內存須要頻繁進行垃圾收集, 而每次回收都要遍歷全部的對象,遍歷這些對象所花費的時間代價是巨大的,會嚴重影響咱們的GC效率,這簡直太可怕了。 有了內存分代,狀況就不一樣了, 1. 新建立的對象會在新生代中分配內存, 2. 通過屢次回收仍然存活下來的對象存放在老年代中,靜態屬性、類信息等存放在永久代中, 3. 新生代中的對象存活時間短,只須要在新生代區域中頻繁進行GC, 4. 老年代中對象生命週期長,內存回收的頻率相對較低,不須要頻繁進行回收, 5. 永久代中回收效果太差,通常不進行垃圾回收 分代收集大大提高了收集效率,這些都是內存分代帶來的好處
一、當年輕代或者老年代滿了,Java虛擬機沒法再爲新的對象分配內存空間了,那麼Java虛擬機就會觸發一次GC去回收掉那些已經不會再被使用到的對象
二、手動調用System.gc()方法,一般這樣會觸發一次的Full GC以及至少一次的Minor GC
三、程序運行的時候有一條低優先級的GC線程,它是一條守護線程,當這條線程處於運行狀態的時候,天然就觸發了一次GC了。
當年輕代內存滿時,會引起一次普通GC,該GC僅回收年輕代。須要強調的時,年輕代盡是指Eden代滿,Survivor滿不會引起GC
當年老代滿時會引起Full GC,Full GC將會同時回收年輕代、年老代
當永久代滿時也會引起Full GC,會致使Class、Method元信息的卸載
(1)調用System.gc時,系統建議執行Full GC,可是沒必要然執行
(2)老年代空間不足
(3)方法區空間不足
(4)經過Minor GC後進入老年代的平均大小大於老年代的可用內存
(5)由Eden區、From Space區向To Space區複製時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小
5. 發現虛擬機頻繁full GC時應該怎麼辦:
(full GC指的是清理整個堆空間,包括年輕代和永久代)
(1) 首先用命令查看觸發GC的緣由是什麼 jstat –gccause 進程id
(2) 若是是System.gc(),則看下代碼哪裏調用了這個方法
(3) 若是是heap inspection(內存檢查),多是哪裏執行jmap –histo[:live]命令
(4) 若是是GC locker,多是程序依賴的JNI庫的緣由
咱們就詳細看一下整個回收過程。
在初始階段,新建立的對象被分配到Eden區,survivor的兩塊空間都爲空。
當Eden區滿了的時候,minor garbage 被觸發
通過掃描與標記,存活的對象被複制到S0,不存活的對象被回收
在下一次的Minor GC中,Eden區的狀況和上面一致,沒有引用的對象被回收,存活的對象被複制到survivor區。然而在survivor區,S0的全部的數據都被複制到S1,須要注意的是,在上次minor GC過程當中移動到S0中的兩個對象在複製到S1後其年齡要加1。此時Eden區S0區被清空,全部存活的數據都複製到了S1區,而且S1區存在着年齡不同的對象,過程以下圖所示:
再下一次MinorGC則重複這個過程,這一次survivor的兩個區對換,存活的對象被複制到S0,存活的對象年齡加1,Eden區和另外一個survivor區被清空。
下面演示一下Promotion過程,再通過幾回Minor GC以後,當存活對象的年齡達到一個閾值以後(可經過參數配置,默認是8),就會被從年輕代Promotion到老年代。
隨着MinorGC一次又一次的進行,不斷會有新的對象被promote到老年代。
上面基本上覆蓋了整個年輕代全部的回收過程。最終,MajorGC將會在老年代發生,老年代的空間將會被清除和壓縮。
年輕代:從上面的過程能夠看出,Eden區是連續的空間,且Survivor總有一個爲空。通過一次GC和複製,一個Survivor中保存着當前還活着的對象,而Eden區和另外一個Survivor區的內容都再也不須要了,能夠直接清空,到下一次GC時,兩個Survivor的角色再互換。所以,這種方式分配內存和清理內存的效率都極高,這種垃圾回收的方式就是著名的「中止-複製(Stop-and-copy)」清理法(將Eden區和一個Survivor中仍然存活的對象拷貝到另外一個Survivor中),這不表明着中止複製清理法很高效,其實,它也只在這種狀況下(基於大部分對象存活週期很短的事實)高效,若是在老年代採用中止複製,則是很是不合適的。
老年代:老年代存儲的對象比年輕代多得多,並且不乏大對象,對老年代進行內存清理時,若是使用中止-複製算法,則至關低效。通常,老年代用的算法是標記-壓縮算法,即:標記出仍然存活的對象(存在引用的),將全部存活的對象向一端移動,以保證內存的連續。在發生Minor GC時,虛擬機會檢查每次晉升進入老年代的大小是否大於老年代的剩餘空間大小,若是大於,則直接觸發一次Full GC,不然,就查看是否設置了-XX:+HandlePromotionFailure
(容許擔保失敗),若是容許,則只會進行MinorGC,此時能夠容忍內存分配失敗;若是不容許,則仍然進行Full GC(這表明着若是設置-XX:+Handle PromotionFailure
,則觸發MinorGC就會同時觸發Full GC,哪怕老年代還有不少內存,因此,最好不要這樣作)。
永久代:永久代是用於存放靜態文件,如Java類、方法等。 關於方法區即永久代的回收,永久代的回收有兩種:常量池中的常量,無用的類信息。持久代對垃圾回收沒有顯著影響,可是有些應用可能動態生成或者調用一些class。永久代的回收並非必須的,能夠經過參數來設置是否對類進行回收。
1. 常量的回收很簡單,沒有引用了就能夠被回收。
2. 類須要同時知足下面3個條件才能算是 「無用的類」 :
虛擬機能夠對知足上述3個條件的無用類進行回收,這裏說的僅僅是「能夠」,而並非和對象同樣不使用了就會必然被回收。
該算法分爲「標記」和「清除」兩個階段: 首先標記出全部須要回收的對象(可達性分析), 在標記完成後統一清理掉全部被標記的對象.這是最基礎的算法,後續的收集算法都是基於這個算法擴展的。
缺點:
此算法把內存空間劃爲兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的對象複製到另一個區域中。此算法每次只處理正在使用中的對象,所以複製成本比較小,同時複製過去之後還能進行相應的內存整理,不會出現「碎片」問題。固然,此算法的缺點也是很明顯的,就是須要兩倍內存空間。效果圖以下:
優勢
缺點
該算法分爲「標記」和「清除」兩個階段: 首先標記出全部須要回收的對象 ( 可達性分析 ), 在標記完成後讓全部存活的對象都向一端移動,而後清理掉端邊界之外的 內存。
此算法結合了「標記-清除」和「複製」兩個算法的優勢。此算法避免了「標記-清除」的碎片問題,同時也避免了「複製」算法的空間問題。效果圖以下:
優勢
缺點
實時垃圾回收算法,即:在應用進行的同時進行垃圾回收。不知道什麼緣由JDK5.0中的收集器沒有使用這種算法的。
把對象分爲年輕代、年老代、持久代(元空間),對不一樣生命週期的對象使用不一樣的算法(上述方式中的一個)進行回收。
爲何要分代?
1. 新生代,每次垃圾回收都會有大量對象死去,可用複製算法,只須要付出少許對象複製成本就能夠完成GC
2. 老年代,存活概率高,沒有額外空間對它進行分配擔保,必須選擇(標記-清除,標記-整理)進行GC
串行收集使用單線程處理全部垃圾回收工做
優勢:無需多線程交互,實現容易,並且效率比較高。
侷限性:沒法使用多處理器的優點
適合單處理器機器。固然,此收集器也能夠用在小數據量(100M左右)狀況下的多處理器機器上。默認使用串行收集器。
指多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態;如ParNew、Parallel Scavenge、Parallel Old;
並行收集使用多線程處理垃圾回收工做
優勢: 速度快,效率高。並且理論上CPU數目越多,越能體現出並行收集器的優點。
適合對吞吐量優先,無過多交互的應用。吞吐量(Throughput)=業務處理時間/(業務處理時間+垃圾回收時間)。
指用戶線程與垃圾收集線程同時執行(但不必定是並行的,可能會交替執行);
用戶程序在繼續運行,而垃圾收集程序線程運行於另外一個CPU上; 如CMS、G1(也有並行);
相對於串行收集和並行收集而言,前面兩個在進行垃圾回收工做時,須要暫停整個運行環境,而只有垃圾回收程序在運行,所以,系統在垃圾回收時會有明顯的暫停,並且暫停時間會由於堆越大而越長。
併發收集器不會暫停應用,適合響應時間優先的應用。
優勢: 保證系統的響應時間,減小垃圾收集時的停頓時間。
適用於應用服務器、電信領域等。
JVM是一個進程,垃圾收集器就是一個線程,垃圾收集線程是一個守護線程,優先級低,其在當前系統空閒或堆中老年代佔用率較大時觸發。
JDK7/8後,HotSpot虛擬機全部收集器及組合(連線),以下圖:
新生代收集器仍是老年代收集器:
吞吐量優先、停頓時間優先
串行並行併發
算法
Serial(串行)垃圾收集器是最基本、發展歷史最悠久的收集器;JDK1.3.1前是HotSpot新生代收集的惟一選擇;
特色: 串行(單線程),新生代,複製算法,STW(Stop the world), 響應速度優先
適用環境:單CPU環境下的Client模式
Tips:單線程一方面意味着它只會使用一個CPU或一條線程去完成垃圾收集工做,另外一方面也意味着在它進行垃圾收集時,必須暫停其餘全部的工做線程,直到它收集結束爲止,這個過程也稱爲 Stop The world。後者意味着,在用戶不可見的狀況下要把用戶正常工做的線程所有停掉,這顯然對不少應用是難以接受的。
Tips:Stop the World是在用戶不可見的狀況下執行的,會形成某些應用響應變慢; Tips:由於新生代的特色是對象存活率低,因此收集算法用的是複製算法,把新生代存活對象複製到老年代,複製的內容很少,性能較好。 Tips:單線程地好處就是減小上下文切換,減小系統資源的開銷,提升效率。但這種方式的缺點也很明顯,在GC的過程當中,會暫停程序的執行。若GC不是頻繁發生,這或許是一個不錯的選擇,不然將會影響程序的執行性能。 對於新生代來講,區域比較小,停頓時間短,因此比較使用。 參數 -XX:+UseSerialGC:串聯收集器 Tips:在JDK Client模式,不指定VM參數,默認是串行垃圾回收器
ParNew收集器就是Serial收集器的多線程版本,它也是一個新生代收集器。
特色: 並行(多線程),新生代,複製算法,STW(Stop the world), 響應速度優先
應用場景:多CPU環境下在Serer模式下與CMS配合
ParNew收集器的工做過程以下圖:
ParNew收集器除了使用多線程收集外,其餘與Serial收集器相比並沒有太多創新之處,但它倒是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關的重要緣由是,除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工做,CMS收集器是JDK 1.5推出的一個具備劃時代意義的收集器,具體內容將在稍後進行介紹。 ParNew 收集器在單CPU的環境中絕對不會有比Serial收集器有更好的效果,甚至因爲存在線程交互的開銷,該收集器在經過超線程技術實現的兩個CPU的環境中都不能百分之百地保證能夠超越。在多CPU環境下,隨着CPU的數量增長,它對於GC時系統資源的有效利用是頗有好處的。 特色 ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集外,其他行爲和Serial收集器徹底同樣,包括Serial收集器可用的全部控制參數、收集算法、Stop The world、對象分配規則、回收策略等都同樣。在實現上也共用了至關多的代碼。
應用場景 ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器。很重要的緣由是:除了Serial收集器以外,目前只有它能與CMS收集器配合工做(看圖)。在JDK1.5時期,HotSpot推出了一款幾乎能夠認爲具備劃時代意義的垃圾收集器-----CMS收集器,這款收集器是HotSpot虛擬機中第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集線程與用戶線程同時工做。
參數 "-XX:+UseConcMarkSweepGC":指定使用CMS後,會默認使用ParNew做爲新生代收集器; "-XX:+UseParNewGC":強制指定使用ParNew; "-XX:ParallelGCThreads":指定垃圾收集的線程數量,ParNew默認開啓的收集線程與CPU的數量相同; 爲何只有ParNew能與CMS收集器配合 CMS是HotSpot在JDK1.5推出的第一款真正意義上的併發(Concurrent)收集器,第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工做; CMS做爲老年代收集器,但卻沒法與JDK1.4已經存在的新生代收集器Parallel Scavenge配合工做; 由於Parallel Scavenge(以及G1)都沒有使用傳統的GC收集器代碼框架,而另外獨立實現;而其他幾種收集器則共用了部分的框架代碼;
Parallel Scavenge垃圾收集器由於與吞吐量關係密切,也稱爲吞吐量收集器(Throughput Collector)。
特色: 並行(多線程),新生代,複製算法,STW(Stop the world), 高吞吐量爲目標
應用場景:在後臺運算而不須要太多交互的任務
Parallel Scavenge收集器和ParNew相似,新生代的收集器,一樣用的是複製算法,也是並行多線程收集。與ParNew最大的不一樣,它關注的是垃圾回收的吞吐量。
應用場景 Parallel Scavenge收集器是虛擬機運行在Server模式下的默認垃圾收集器。 高吞吐量爲目標,即減小垃圾收集時間,讓用戶代碼得到更長的運行時間;適合那種交互少、運算多的場景 例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程序; 參數 "-XX:+MaxGCPauseMillis":控制最大垃圾收集停頓時間,大於0的毫秒數;這個參數設置的越小,停頓時間可能會縮短,但也會致使吞吐量降低,致使垃圾收集發生得更頻繁。 "-XX:GCTimeRatio":設置垃圾收集時間佔總時間的比率,0<n<100的整數,就至關於設置吞吐量的大小。 先垃圾收集執行時間佔應用程序執行時間的比例的計算方法是:1 / (1 + n);
例如,選項-XX:GCTimeRatio=19,設置了垃圾收集時間佔總時間的5%=1/(1+19); 默認值是1%--1/(1+99),即n=99; 垃圾收集所花費的時間是年輕一代和老年代收集的總時間; "-XX:+UseAdptiveSizePolicy" 開啓這個參數後,就不用手工指定一些細節參數,如:
新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、晉升老年代的對象年齡(-XX:PretenureSizeThreshold)等; JVM會根據當前系統運行狀況收集性能監控信息,動態調整這些參數,以提供最合適的停頓時間或最大的吞吐量,這種調節方式稱爲GC自適應的調節策略(GC Ergonomiscs); Parallel Scavenge收集器 VS CMS等收集器: Parallel Scavenge收集器的特色是它的關注點與其餘收集器不一樣,CMS等收集器的關注點是儘量地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。 因爲與吞吐量關係密切,Parallel Scavenge收集器也常常稱爲「吞吐量優先」收集器。
另外值得注意的一點是,Parallel Scavenge收集器沒法與CMS收集器配合使用,因此在JDK 1.6推出Parallel Old以前,若是新生代選擇Parallel Scavenge收集器,老年代只有Serial Old收集器能與之配合使用。
Parallel Scavenge收集器 VS ParNew收集器:
Parallel Scavenge收集器與ParNew收集器的一個重要區別是它具備自適應調節策略。
Serial Old是 Serial收集器的老年代版本
特色: 串行(單線程),老年代,標記-整理算法,STW(Stop the world),響應速度優先
應用場景:單CPU環境下的Client模式、CMS的後備預案
Serial收集器的工做流程以下圖:
如上圖所示,Serial 收集器在新生代和老年代都有對應的版本,除了收集算法不一樣,兩個版本並無其餘差別。 Serial 新生代收集器採用的是複製算法。 Serial Old 老年代採用的是標記 - 整理算法。 應用場景 Client模式:Serial Old收集器的主要意義也是在於給Client模式下的虛擬機使用。 Server模式:若是在Server模式下,那麼它主要還有兩大用途:
一種用途是在JDK 1.5以及以前的版本中與Parallel Scavenge收集器搭配使用;
另外一種用途就是做爲CMS收集器的後備預案,在併發收集發生"Concurrent Mode Failure"時使用。
Parallel Old收集器是Parallel Scavenge收集器的老年版本,它也使用多線程和「標記-整理」算法。這個收集器是在JDK 1.6開始提供。
特色: 並行(多線程),老年代,標記-整理算法(還有壓縮,Mark-Sweep-Compact),STW(Stop the world),吞吐量優先
應用場景:在後臺運算而不須要太多交互的任務
如上圖所示,Parallel 收集器在新生代和老年代也都有對應的版本,除了收集算法不一樣,兩個版本並無其餘差別。 特色 Parallel Old是Parallel Scavenge的老年代版本 Parallel Old 老年代採用的是標記 - 整理算法,其餘特色與Parallel Scavenge相同 使用場景 在注重吞吐量以及CPU資源敏感的場合,均可以優先考慮Parallel Scavenge加Parallel Old收集器組合。 JDK1.6及以後用來代替老年代的Serial Old收集器; 特別是在Server模式,多CPU的狀況下; 參數 -XX:+UseParallelOldGC:指定使用Parallel Old收集器;
CMS是HotSpot在JDK5推出的第一款真正意義上的併發(Concurrent)收集器,第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工做;
特色: 併發(多線程),老年代,標記-清除算法 (不進行壓縮操做,產生內存碎片),收集過程當中不須要暫停用戶線程,以獲取最短回收停頓時間爲目標
應用場景:與用戶交互較多的場景,互聯網或者B/S系統,重視響應速度和用戶體驗的應用
它關注的是垃圾回收最短的停頓時間(低停頓),在老年代並不頻繁GC的場景下,是比較適用的。
CMS,全稱Concurrent Mark and Sweep,用於對年老代進行回收,目標是儘可能減小應用的暫停時間,減小full gc發生的機率,利用和應用程序線程併發的垃圾回收線程來標記清除年老代
CMS並不是沒有暫停,而是用兩次短暫停來替代串行標記整理算法的長暫停。
應用場景 與用戶交互較多的場景。CMS 收集器是一種以獲取最短回收停頓時間爲目標的收集器。
目前很大一部分的Java應用集中在互聯網或者B/S系統的服務端上,這類應用尤爲注重服務的響應速度,但願系統停頓時間最短,以給用戶帶來極好的體驗。 CMS是一種以獲取最短回收停頓時間爲目標的收集器。在重視響應速度和用戶體驗的應用中,CMS應用不少。
參數
-XX:+UseConcMarkSweepGC:使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection:Full GC後,進行一次碎片整理;整理過程是獨佔的,會引發停頓時間變長
-XX:+CMSFullGCsBeforeCompaction:設置進行幾回Full GC後,進行一次碎片整理
-XX:ParallelCMSThreads:設定CMS的線程數量(通常狀況約等於可用CPU數量)
CMS GC收集週期分四步完成: 初始標記(initial mark)STW 耗時最短,併發標記(concurrent mark)耗時最長,從新標記(remark)STW 耗時較長,併發清除(concurrent sweep)耗時最長.
一、初始標記(initial mark)
這個階段的任務是標記老年代中被GC Roots直接可達和被年輕代對象引用的對象,這個階段也是第一次STW發生的階段
特色:單線程執行;須要「Stop The World」;僅把GC Roots的直接關聯可達的對象給標記一下,因爲直接關聯對象比較小,因此這裏的速度很是快。
二、併發標記(concurrent mark)
這個階段主要是經過從初始標記階段中尋找到的標記對象開始,遍歷老年代而且標記全部存活着的對象(可達性分析)
特色:這個階段與應用程序共同運行,其餘線程仍能夠繼續工做。此處時間較長,但不停頓。並不能保證能夠標記出全部的存活對象;
須要注意的是,並不是全部在老年代中存活的對象都會被標記,由於程序在標記期間可能會更改引用(好比圖中的Current obj,它是併發標記階段伴隨着程序一塊兒被刪除了引用的對象)
三、Concurrent Preclean 執行預清理
注: 至關於兩次 concurrent-mark. 由於上一次concurrent-mark耗時較長,會有重新生代晉升到老年代的對象出現,將其清理掉
這也是一個併發階段,與應用程序的線程並行執行。併發標記階段與應用程序同時運行時,一些對象的引用可能會被改變,一旦這種狀況發生,JVM就會標記堆上面的這塊包含了變化對象的區域(這個堆的區域被稱爲"Card",這種方式被稱爲"Card Marking")
在這個階段,這些髒對象將會被聲明,而且這些對象可以到達的對象也會被標記。這些Card將會在上面的工做完成以後被清理掉
此外,還將執行一些必要的整理和從新標記階段的準備工做。
四、Concurrent Abortable Preclean 執行可停止預清理
這個階段也是和程序線程併發執行的。它的工做就是儘量地進行清理工做,以減小從新標記階段的任務(即減小了STW的停頓時間)
這個階段的持續時間取決於不少因素,由於它須要不斷地作一些相同的工做,直到知足某個終止條件爲止(好比必定的迭代次數、必定的有效工做量、必定的時間等等)
五、從新標記(Final remark)
這個階段是第二次,也是最後一次STW。這個階段的目的是標記在老年代中被標記的全部存活下來的對象。
在併發標記的過程當中,因爲可能還會產生新的垃圾,因此此時須要從新標記新產生的垃圾。
此處執行並行標記,與用戶線程不併發,因此依然是「Stop The World」,且停頓時間比初始標記稍長,但遠比並發標記短。
六、併發清除(concurrent sweep)
併發清除以前所標記的垃圾,移除未使用的對象,而且回收其佔用的空間。其餘用戶線程仍能夠工做,不須要停頓。
七、Concurrent Reset 併發重置
重置CMS算法內部的數據結構,爲下一個週期作準備
八、總結
Tips:其中,初始標記和重寫標記仍然須要Stop the World、初始標記僅僅標記一下GC Roots能直接關聯到的對象,速度很快,併發標記就是進行GC RootsTracing的過程,而從新標記階段則是爲了修正併發標記期間因用戶程序繼續運行而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段長,但遠比並發標記的時間短。
因爲整個過程當中耗時最長的併發標記和併發清除過程收集器線程均可以與用戶線程一塊兒工做,因此總體上說,CMS收集器的內存回收過程是與用戶線程一共併發執行的。
一、對CPU資源很是敏感
對CPU資源很是敏感 其實,面向併發設計的程序都對CPU資源比較敏感。
在併發階段,它雖然不會致使用戶線程停頓,但會由於佔用了一部分線程(或者說CPU資源)而致使應用程序變慢,總吞吐量會下降。
CMS默認啓動的回收線程數是(CPU數量+3)/ 4,
也就是當CPU在4個以上時,併發回收時垃圾收集線程很多於25%的CPU資源,而且隨着CPU數量的增長而降低。
可是當CPU不足4個時(好比2個),CMS對用戶程序的影響就可能變得很大,若是原本CPU負載就比較大,還要分出一半的運算能力去執行收集器線程,就可能致使用戶程序的執行速度突然下降了50%,其實也讓人沒法接受。
二、浮動垃圾(Floating Garbage)
因爲CMS併發清理階段用戶線程還在運行着,伴隨程序運行天然就還會有新的垃圾不斷產生,這一部分垃圾出如今標記過程以後,CMS沒法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱爲「浮動垃圾」。
因爲在垃圾收集階段用戶線程還須要運行,那就還須要預留有足夠的內存空間給用戶線程使用,所以CMS收集器不能像其餘收集器那樣等到老年代幾乎徹底被填滿了再進行收集,也能夠認爲CMS所須要的空間比其餘垃圾收集器大;
"-XX:CMSInitiatingOccupancyFraction":設置CMS預留內存空間;
JDK1.5默認值爲68%;
JDK1.6變爲大約92%;
三、"Concurrent Mode Failure"失敗
若是CMS運行期間預留的內存沒法知足程序須要,就會出現一次「Concurrent Mode Failure」失敗,這時虛擬機將啓動後備預案:臨時啓用Serial Old收集器來從新進行老年代的垃圾收集,這樣會致使另外一次Full GC的產生。這樣停頓時間就更長了,代價會更大,因此 "-XX:CMSInitiatingOccupancyFraction"不能設置得太大。
四、產生大量內存碎片
這個問題並非CMS的問題,而是算法的問題。因爲CMS基於"標記-清除"算法,清除後不進行壓縮操做,因此會產生碎片
"標記-清除"算法介紹時曾說過:產生大量不連續的內存碎片會致使分配大內存對象時,沒法找到足夠的連續內存,從而須要提早觸發另外一次Full GC動做。
碎片解決方法:
(1)"-XX:+UseCMSCompactAtFullCollection"
使得CMS出現上面這種狀況時不進行Full GC,而開啓內存碎片的合併整理過程;
但合併整理過程沒法併發,停頓時間會變長;
默認開啓(但不會進行,結合下面的CMSFullGCsBeforeCompaction);
(2)"-XX:+CMSFullGCsBeforeCompaction"
設置執行多少次不壓縮的Full GC後,來一次壓縮整理;
爲減小合併整理過程的停頓時間;
默認爲0,也就是說每次都執行Full GC,不會進行壓縮整理;
因爲空間再也不連續,CMS須要使用可用"空閒列表"內存分配方式,這比簡單實用"碰撞指針"分配內存消耗大;
整體來看,與Parallel Old垃圾收集器相比,CMS減小了執行老年代垃圾收集時應用暫停的時間;但卻增長了新生代垃圾收集時應用暫停的時間、下降了吞吐量並且須要佔用更大的堆空間;最大的優勢就是低停頓。
CMS減小停頓的原理: 標記過程分三步:
併發標記是最主要的標記過程,而這個過程是併發執行的,能夠與應用程序線程同時進行,
初始標記和從新標記雖然不能和應用程序併發執行,但這兩個過程標記速度快,時間短,因此對應用程序不會產生太大的影響。
最後併發清除的過程,也是和應用程序同時進行的,避免了應用程序的停頓。 CMS的特色: 減小了應用程序的停頓時間,讓回收線程和應用程序線程能夠併發執行,它的回收並不完全(浮動垃圾)。
所以CMS回收的頻率相較其餘回收器要高,頻繁的回收將影響應用程序的吞吐量,空間碎片多。 CMS什麼時候開始? cms gc 經過一個後臺線程觸發,該線程隨着堆一塊兒初始化,觸發機制是默認每隔2秒判斷一下當前老年代的內存使用率是否達到閾值,若是高於某個閾值的時候將激發CMS。 兩次STW的緣由: 當虛擬機完成兩次標記後,便確認了能夠回收的對象。
垃圾回收並不會阻塞程序的線程,若是當GC線程標記好了一個對象的時候,此時程序的線程又將該對象從新加入了GC-Roots的「關係網」中,當執行二次標記的時候,該對象也沒有重寫finalize()方法,所以回收的時候就會回收這個不應回收的對象。 爲了解決這個問題,虛擬機會在一些特定指令位置設置一些「安全點」,當程序運行到這些「安全點」的時候就會暫停全部當前運行的線程(Stop The World 因此叫STW),暫停後再找到「GC Roots」進行關係的組建,進而執行標記和清除。 這些特定的指令位置主要在: 一、循環的末尾 二、方法臨返回前 / 調用方法的call指令後 三、可能拋異常的位置
G1(Garbage - First)名稱的由來是G1跟蹤各個Region裏面的垃圾堆的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的Region。
G1(Garbage-First)是JDK7-u4才推出商用的收集器;
注意:G1與前面的垃圾收集器有很大不一樣,它把新生代、老年代的劃分取消了!這樣咱們不再用單獨的空間對每一個代進行設置了,不用擔憂每一個代內存是否足夠。
取而代之的是,G1算法將堆劃分爲若干個區域(Region),它仍然屬於分代收集器。不過,這些區域的一部分包含新生代,新生代的垃圾收集依然採用暫停全部應用線程的方式,將存活對象拷貝到老年代或者Survivor空間。老年代也分紅不少區域,G1收集器經過將對象從一個區域複製到另一個區域,完成了清理工做。這就意味着,在正常的處理過程當中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有CMS內存碎片問題的存在了。
在G1中,還有一種特殊的區域,叫Humongous區域。 若是一個對象佔用的空間超過了分區容量50%以上,G1收集器就認爲這是一個巨型對象。這些巨型對象,默認直接會被分配在年老代,可是若是它是一個短時間存在的巨型對象,就會對垃圾收集器形成負面影響。爲了解決這個問題,G1劃分了一個Humongous區,它用來專門存放巨型對象。若是一個H區裝不下一個巨型對象,那麼G1會尋找連續的H分區來存儲。爲了能找到連續的H區,有時候不得不啓動Full GC。
PS:在java 8中,持久代也移動到了普通的堆內存空間中,改成元空間。
G1除了下降停頓外,還能創建可預測的停頓時間模型;
一、Region概念
二、可並行,可併發
三、分代收集
四、結合多種垃圾收集算法(空間整合,不產生碎片)
五、可預測的停頓:低停頓的同時實現高吞吐量
若是你的應用追求低停頓,那G1如今已經能夠做爲一個可嘗試選擇,若是你的應用追求吞吐量,那G1並不會爲你帶來什麼特別的好處。
1. 面向服務端應用,針對具備大內存、多處理器的機器;最主要的應用是爲須要低GC延遲,並具備大堆的應用程序提供解決方案;
如:在堆大小約6GB或更大時,可預測的暫停時間能夠低於0.5秒;
2. 用來替換掉JDK1.5的CMS收集器;
(1)超過50%的Java堆被活動數據佔用;
(2)對象分配頻率或年代提高頻率變化很大;
(3)GC停頓時間過長(長與0.5至1秒)。
"-XX:+UseG1GC":指定使用G1收集器;
"-XX:InitiatingHeapOccupancyPercent":當整個Java堆的佔用率達到參數值時,開始併發標記階段;默認爲45;
"-XX:MaxGCPauseMillis":爲G1設置暫停時間目標,默認值爲200毫秒;
"-XX:G1HeapRegionSize":設置每一個Region大小,範圍1MB到32MB;目標是在最小Java堆時能夠擁有約2048個Region;
不計算維護Remembered Set的操做,能夠分爲4個步驟(與CMS較爲類似)。
一、初始標記(Initial Marking)
二、併發標記(Concurrent Marking)
三、最終標記(Final Marking)
四、篩選回收(Live Data Counting and Evacuation)
在JDK 11當中,加入了實驗性質的ZGC。它的回收耗時平均不到2毫秒。它是一款低停頓高併發的收集器。
ZGC幾乎在全部地方併發執行的,除了初始標記的是STW的。因此停頓時間幾乎就耗費在初始標記上,這部分的實際是很是少的。那麼其餘階段是怎麼作到能夠併發執行的呢?
ZGC主要新增了兩項技術,一個是着色指針Colored Pointer,另外一個是讀屏障Load Barrier。
ZGC 是一個併發、基於區域(region)、增量式壓縮的收集器。Stop-The-World 階段只會在根對象掃描(root scanning)階段發生,這樣的話 GC 暫停時間並不會隨着堆和存活對象的數量而增長。
與標記對象的傳統算法相比,ZGC在指針上作標記,在訪問指針時加入Load Barrier(讀屏障),
好比當對象正被GC移動,指針上的顏色就會不對,這個屏障就會先把指針更新爲有效地址再返回,
也就是,永遠只有單個對象讀取時有機率被減速,而不存在爲了保持應用與GC一致而粗暴總體的Stop The World。
ZGC雖然目前還在JDK 11還在實驗階段,但因爲算法與思想是一個很是大的提高,相信在將來不久會成爲主流的GC收集器使用。
ZGC回收機預計在jdk11支持,ZGC目前僅適用於Linux / x64 。和G1開啓很像,用下面參數便可開啓:
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
這裏的併發(Concurrent),說的是應用線程與GC線程齊頭並進,互不添堵。
幾乎就是還有三個很是短暫的STW的階段,因此ZGC並非Zero Pause GC啦。好比開始的Pause Mark Start階段,要作根集合(root set)掃描,包括全局變量啊、線程棧啊啥的裏面的對象指針,但不包括GC堆裏的對象指針,因此這個暫停就不會隨着GC堆的大小而變化(不過會根據線程的多少啊、線程棧的大小之類的而變化)」。
ZGC利用指針的64位中的幾位表示Finalizable、Remapped、Marked一、Marked0(ZGC僅支持64位平臺),以標記該指向內存的存儲狀態。至關於在對象的指針上標註了對象的信息。注意,這裏的指針至關於Java術語當中的引用。(因此它不支持32位指針也不支持壓縮指針, 且堆的上限是4TB。)
在這個被指向的內存發生變化的時候(內存在Compact被移動時),顏色就會發生變化。
在G1的時候就說到過,Compact階段是須要STW,不然會影響用戶線程執行。那麼怎麼解決這個問題呢?
因爲着色指針的存在,在程序運行時訪問對象的時候,能夠輕易知道對象在內存的存儲狀態(經過指針訪問對象),若請求讀的內存在被着色了,那麼則會觸發讀屏障。讀屏障會更新指針再返回結果,此過程有必定的耗費,從而達到與用戶線程併發的效果。
ZGC將堆劃分爲Region做爲清理,移動,以及並行GC線程工做分配的單位。
不過G1一開始就把堆劃分紅固定大小的Region,而ZGC 能夠有2MB,32MB,N× 2MB 三種Size Groups,動態地建立和銷燬Region,動態地決定Region的大小。
256k如下的對象分配在Small Page, 4M如下對象在Medium Page,以上在Large Page。
因此ZGC能更好的處理大對象的分配。
CMS是Mark-Sweep標記過時對象後原地回收,這樣就會形成內存碎片,愈來愈難以找到連續的空間,直到發生Full GC才進行壓縮整理。
ZGC是Mark-Compact ,會將活着的對象都移動到另外一個Region,整個回收掉原來的Region。
而G1 是 incremental copying collector,同樣會作壓縮。
1. Pause Mark Start -初始停頓標記
停頓JVM地標記Root對象,1,2,4三個被標爲live。
2. Concurrent Mark -併發標記
併發地遞歸標記其餘對象,5和8也被標記爲live。
3. Relocate - 移動對象
對比發現三、六、7是過時對象,也就是中間的兩個灰色region須要被壓縮清理,因此陸續將四、五、8 對象移動到最右邊的新Region。
移動過程當中,有個forward table紀錄這種轉向。
活的對象都移走以後,這個region能夠當即釋放掉,而且用來看成下一個要掃描的region的to region。因此理論上要收集整個堆,只須要有一個空region就OK了。
4. Remap - 修正指針
最後將指針都妥帖地更新指向新地址。上一個階段的Remap,和下一個階段的Mark是混搭在一塊兒完成的,這樣很是高效,省卻了重複遍歷對象圖的開銷。」
G1 保證「每次GC停頓時間不會過長」的方式,是「每次只清理一部分而不是所有的Region」的增量式清理。那獨立清理某個Region時 , 就須要有RememberSet來記錄Region之間的對象引用關係, 這樣就能依賴它來輔助計算對象的存活性而不用掃描全堆, RS一般佔了整個Heap的20%或更高。
這裏還須要使用Write Barrier(寫屏障)技術,G1在平時寫引用時,GC移動對象時,都要同步去更新RememberSet,跟蹤跨代跨Region間的引用,特別的重。而CMS裏只有新老生代間的CardTable,要輕不少。
ZGC幾乎沒有停頓,因此劃分Region並非爲了增量回收,每次都會對全部Region進行回收,因此也就不須要這個佔內存的RememberSet了,又由於它暫時連分代都還沒實現,因此徹底沒有Write Barrier。
沒分代,應該是ZGC惟一的弱點了。
分代本來是由於most object die young的假設,而讓新生代和老生代使用不一樣的GC算法。
若是對整個堆作一個完整併發收集週期,持續的時間可能很長好比幾分鐘,而此期間新建立的對象,大體上只能看成活對象來處理,即便它們在這週期裏其實早就死掉能夠被收集了。若是有分代算法,新生對象都在一個專門的區域建立,專門針對這個區域的收集能更頻繁更快,意外留活的對象更也少。