JVM進行垃圾回收時要考慮哪的問題以下:java
1.如何斷定對象爲垃圾對象?算法
1.引用計數法:在對象中添加一個引用計數器,當有地方引用這個對象的時候,引用計數器的值就+1,引用失效的時候,計數器的值就-1,服務器
直到計數器的值爲0時,就被垃圾回收器回收。這種方式實現簡單,斷定效率也是比較高的,單是但遇到一種狀況就不行了,好比說堆中的對象實例相互引用,斷開被棧引用。這樣因爲數據結構
堆中實例對象相互引用,而引用計數器的值卻不會不會變成0。這種方式致使沒法回收。目前爲止,java的回收器基本沒有使用這種算法。咱們用代碼看一下,到底有沒有用這種算法。以下代碼:多線程
package hjc.test7; public class Main { private Object instance; public Main() { byte [] m = new byte[20*1024*1024]; } public static void main(String[] args) { Main m1 = new Main(); Main m2 = new Main(); m1.instance = m2; m2.instance = m1; m1 = null; m2 = null; System.gc(); } }
在虛擬機配置以下參數,如圖所示:併發
結果以下圖:框架
能夠看到堆中的對象實例相互引用,斷開被棧引用的這部分確實被回收了,這也證實了java回收器中沒用這種算法。性能
2.可達性分析法。網站
這個算法的基本思路就是經過一系列的稱爲"GC Roots"的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑成爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連(用數據結構的圖論來講,就是從GC Roots到達這個對象不可達)時,線程
則證實此對象是不可用的。以下圖所示:對象object5,6,7雖然互相有關聯,可是他們到GC Roots是不可達的,因此他們將會被判斷爲是可回收的對象。
在JAVA語言中,可做爲GC Roots的對象包括下面幾種:
1.虛擬機棧(棧幀的本地變量表)中引用的對象。
2.方法區中類靜態屬性引用的對象。
這裏還要說一些細節:
不管是經過引用計數算法判斷對象的引用數量,仍是經過可達性分析算法判斷對象的引用鏈是否可達,判定對象是否存活都與「引用」有關。在JDK1.2之前,JAVA的引用定義很傳統:若是reference類型的數據中存儲的數值表明的是另一個塊內存的起始地址,
這塊內存表明着一個引用。這種定義很存粹,太過狹隘,一個對象在這種定義下只有被引用或者沒有被引用兩種狀態,對於如何描述一些「食之無味,棄之惋惜」的對象就顯得無能爲力。所以咱們但願描述這樣一類對象:當內存空間還足夠時,可以保留在內存中;
若是內存空間在垃圾收集後仍是很是緊張,則能夠拋棄這些對象。
Java在JDK1.2對引用的概念進行了擴充,將引用分爲強引用,軟引用,弱引用,虛引用。
強引用:指在程序代碼之中普片存在的,相似「Object obj = new Object()」這類的引用,只要強引用還存在,垃圾收集齊永遠不會回收被引用的對象。
軟引用:用來描述一些還有用但並不是必須的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,會把這些對象列進回收範圍之中進行第二次回收。若是此次回收尚未足夠的內存,纔會拋出內存溢出異常。
在JDK1.2以後,提供了SoftReference類來實現軟引用。
弱引用:也是用來描述非必需對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾收集齊工做時,不管當前內存是否足夠,都會回收掉被弱引用關聯的對象。在JDK1.2以後,提供了
WeakReference類來實現弱引用。
虛引用:稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設計虛引用關聯的惟一目的就是能在這個對象被收集器回收時
收到一個系統通知。提供了PhantomRefernce類來實現虛引用。
斷定一個類是否爲"無用的類"必須同時知足3個條件才行。
1.該類全部的實例都已經被回收,也就是Java堆中不存在該類的任何實例。
2.加載該類的ClassLoader已經被回收了。
3.該類的對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。
虛擬機知足上面3個條件的無用類進行回收,注意,僅僅是」能夠「,而並非和對象同樣,不使用了就必然會回收。HostSpot虛擬機提供了 -Xnoclassgc 參數進行控制,還可使用-verbose:class以及-XX:TraceClassLoading,-XX:+TraceClassUnLoading查看類
加載和卸載信息,其中-verbose:class和-XX:TraceClassLoading能夠在Product版的虛擬機中使用,-XX:TraceClassUnLoading參數須要FastDebug版的虛擬機支持。
在大量使用反射,動態代理,CGLib等ByteCode框架,動態生成JSP以及OSGi這類頻繁自動義ClassLoader的場景都須要虛擬機具有類卸載的功能,以保證永久代不會溢出。
2.如何回收?
1.回收策略。
1.標記-清除算法
算法分爲」標記「和」清楚「兩耳階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象,它的標記過程在前面的筆記已經出現過了。它是最基礎的收集算法,由於後面的算法都是基於這種思路並對其不足進行改進而獲得的。
它主要有兩個不足:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早
觸發另一次垃圾收集動做。標記-清楚算法執行過程以下圖:
2.複製算法。
爲了解決效率問題,一種稱爲」複製「的收集算法出現了,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。
這樣使得每次都是對整個半區進行內存回收,內存分配時,也就不用考慮內存碎片等複雜狀況,只要移動堆指針,按順序分配內存便可,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲了原來的一半,未免過高了一點。
算法執行過程以下圖:
IBM公司的專門研究代表,新生代中的對象98%是」朝生夕死「的,因此並不須要按照1:1的比例來劃份內存空間,而是將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一個塊Survivor。
當回收時,將Eden和Survivor中還存活着的對象一次性的複製到另一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HostSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是說每次新生代中可用
內存空間爲真個新生代容量的90%,只有10%內存會被浪費。當另一片Survior空間不夠用時,須要依賴其餘內存(老年代)進行分配擔保,所以另一片Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接經過分配擔保機制進入老年代。
3.標記-整理算法
複製手機算法在對象存活率較高時就要進行較多的複製操做,效率將會變低,更關鍵的是,若是不想浪費50%的空間,就須要有額外的空間進行非配擔保,以應對被使用的內存中全部對象都100%存活的極端狀況,因此在老年代通常不能直接選用這種算法。
標記-整理算法仍然與標記-清除算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活對象都向一端移動,而後直接清理掉端邊界之外的內存,標記-整理算法,以下圖所示:
4.分代收集算法
這種算法思想是根據對象存活週期的不一樣將內存劃分爲幾塊,通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最合適的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就算用賦值算法。
只須要付出少許存活對象的複製成本就能夠完成收集。而老年代總由於對象存活率搞,沒有額外空間對他進行分配擔保,就必須使用"標記-清理"或者」標記整理「算法來回收。
2.垃圾回收器
1.Serial
Serial曾經是在JDK1.3以前是虛擬機新生代收集的惟一選擇,是一個單線程的收集器,但它的單線程的意義並不只僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工做,更重要的是它在運行垃圾收集時,必須暫停其餘全部的工做線程,
直到它收集結束。從JDK1.3到如今最新的1.9,HostSpot虛擬機團隊爲消除或者減小工做線程因內存回收而致使停頓的努力一直在進行着從Serial到Parallel收集器,再從Concurrent Mark Sweep(CMS)到GC收集器最前沿成果Garbage First(G1)收集器,
用戶線程的停頓時間在不斷縮短,可是仍然沒辦法徹底消除(這裏暫不包括RTSJ中的收集器)。到如今爲止,它依然是虛擬機運行在Client模式下的默認新生代收集器。它的優勢在於:簡單而高效(與其餘收集器的單線程比),對於限定單個CPU的環境來講
Serial收集器因爲沒有線程交互的開銷,專心作垃圾收集天然能夠得到最高的單線程收集效率。在用戶的桌面應用場景中,分配給虛擬機管理的內存通常來講不會很大,收集幾十兆到一兩百兆的新生代(僅僅是新生代使用的內存),停頓時間是徹底能夠控制
在即時毫秒最多一百多毫秒之內,只要不要頻繁發生,這停頓是能夠接受的。因此Serial對於運行在Client模式下的虛擬機來講是一個很好的選擇。以下圖:
2.Parnew
Parnew收集器其實就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集以外,其他行爲包括Serial收集器可用的全部控制參數(例如:-XX:SurvivorRatio,-XX:PretenureSizeThreshold,-XX:HandlePromotionFailure等),收集算法,Stop the World,對象分配規則
,回收策略等都與Serial收集器徹底同樣,共用了至關多的代碼。其工做過程以下圖:
Parnew收集器倒是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關但很重要的緣由是,除了Serial收集齊外,目前只有它能與CMS收集器配合工做。
注意:Parnew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至因爲存在線程交互的開銷,該收集器在經過超線程技術實現的兩個CPU的環境中都不能百分之百地保證超過Serial收集器。
對於使用的CPU的數量的增長,它對於GC時系統資源的有效利用仍是頗有好處的。它默認安康i其的手機線程數與CPU的數量相同,在CPU很是多的環境下,可使用-XX:ParalleLGCThreads參數來限制垃圾收集器的線程數。
3.Parallel Scaveng
這裏先說一下垃圾收集器環境下什麼是並行,什麼是併發。
並行:指多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態。
併發:指用戶線程與垃圾收集線程同時執行(但並不必定並行,可能會交替執行),用戶程序在繼續運行,並且垃圾收集程序運行於另一個CPU上。
Parallel Scaveng收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器,它的關注點與其餘收集器不一樣,CMS等收集器的關注點是儘量地縮短垃圾收集時用戶線程的停頓時間,
而Parallel Scaveng收集器的目標則是達到一個可控制的吞吐量。所謂的吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼/(用戶代碼時間+垃圾收集時間)。虛擬機總共運行100分鐘,其中垃圾收集花掉1分鐘,
那吞吐量就是99%。停頓時間越短越適合與用戶交互的程序,良好的響應速度能提高用戶體驗,而高吞吐量能夠高效率地利用CPU時間,儘快完成程序的運算任務。
Parallel Scaveng提供了兩個參數用戶精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMilis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數。
注意:MaxGCPauseMilis參數容許的值是一個大於0的毫秒數,收集器將盡量地保證內存回收花費的時間不超設置值。不要認爲這個參數的值設置越小就能使垃圾收集速度變得更快,GC停頓時間短是以犧牲吞吐量和新生代空間換取的;
GCTimeRatio參數的值應該是一個大於0且小於100的整數,也就是垃圾收集時間佔總時間的比率,至關因而吞吐量的倒數。若是把此參數設置爲19,那容許額最大GC時間就佔總時間的5%(即1/(1+19)),默認值爲99,就是容許最大1%(即1/(1+99))的垃圾收集時間。
因爲與吞吐量關係密切,它一般被稱爲「吞吐量優先」收集器。Parallel Scavenge收集器還有一個參數-XX:UseAdaptiveSizePolicy值得關注,這是一個開關參數,當這個參數打開後,就不須要手工指定新生代的大小(-Xmm),Eden與Survivor區的比例(-XX;SurvivorRatio)
晉升老年代對象年齡(-XX:PretenureSizehold)等參數細節,虛擬機會根據當前系統運行狀況收集性能監控信息,動態調整這些參數以提供最適合的停頓時間或者最大的吞吐量,這種方式稱爲GC自適應調節策略。
4.Serial Old
Serial Old是Serial收集器的老年代版本,它一樣是一個單線程收集器,使用"標記-整理算法",這個收集器的主要意義也是在於給Client模式下的虛擬機使用。若是在Server模式下,那麼它主要有兩大用途:一種用途就是在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,
另一種用途就是做爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。工做流程以下:
5.Parallel Old
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和「標記-整理」算法。這個收集器是在JDK1.6中才開始提供的,在此以前,新生代的Parallel Scavenge收集器一致處於比較尷尬額狀態。緣由是,若是新生代選擇了Parallel Scavenge收集器,老年代
除了Serial Old收集器外別無選擇。因爲老年代Serial Old收集器在服務端應用性能上拖累,使用了Parallel Scavenge收集器也未必能在總體應用上得到吞吐量最大化的效果。因爲單線程的老年代收集中沒法充分利用服務器多CPU的處理能力,在老年代很大並且硬件
比較高級的環境中,這種組合的吞吐量甚至還不必定有ParNew+CM組合給力。直到Paralllel Old 收集器出現後,「吞吐量優先」收集器終於有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,均可以有限考慮Parallel Scavenge+Parallel Old收集器。
它的工做流程圖以下:
6.Cms
CMS收集器是一種以得到最短停頓時間爲目標的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤爲重視服務的響應速度,但願系統停頓時間最短,已給用戶帶來較好的體驗。
CMS收集器是基於「標記-清除算法」實現的,,整個過程分爲4個步驟:
1.初始標記
2.併發標記
3.從新標記
4.併發清除
注意:初始標記,重標記這兩個不走仍然須要「Stop The World」。初始標記僅僅是標記一下GC Roots能直接關聯到的對象,毒素很快,併發標記階段就是GC Roots Tracing的過程,而從新標記則是爲了修正併發標記期間因用戶程序繼續運做而致使
標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但遠比並發標記的時間短。
因爲整個過程當中耗時最長的併發標記和併發消除過程收集器線程均可以與用戶線程一塊兒工做,因此,整體上來講,CMS收集器的內存回收過程是與用戶線程一塊兒併發執行的。
下面經過CMS收集器看到運做步驟中併發和須要停頓的時間:
CMS有三個明顯的缺點:
1.CMS收集器對CPU資源很是敏感,在併發階段,他雖然不會致使用戶線程停頓,可是會由於佔用一部分線程(或者說CPU資源)而致使應用程序變慢,總吞吐量會下降。
2.CMS收集器沒法處理浮動垃圾,可能出現"Concurrent Mode Failure"失敗而致使另一次Full GC的產生。因爲CMS併發清理階段用戶線程還在運行着,伴隨程序運行天然就還會有新的垃圾不斷產生,這一部分垃圾出如今標記過程以後,
CMS沒法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就成爲浮動垃圾。因爲在垃圾收集階段用戶線程還須要運行,那也就還須要預留有足夠的內存空間給用戶線程使用,所以CMS不能像其餘收集器那樣
等到老年代幾乎徹底被填滿了再進行收集,須要預留一部分空間提供併發收集時的程序運做使用。在JDK1.5的默認設置下,CMS收集器當老年代使用了68%的空間後就會被激活,若是在應用中老年代增加不是很快,能夠調高參數
-XX:CMSinitiatingOccupancyFraction的值來提升觸發百分比,以便下降內存回收次數來得到更好的性能,在JDK1.6,啓動閾值已經提高至92%。注意,-XX:CMSinitiatingOccupancyFraction的值設置過高,容易致使大量的"Concurrent Mode Failure"失敗,性能反而降低。
3.因爲是基於「標記-清除」算法實現的,這意味着收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,每每致使會出現老年代還有很大空間剩餘,可是沒法找到足夠打的連續空間來分配當前對象。不得不提早出發一次Full GC。
爲了解決這問題,CMS收集器提供一個-XX:+UseCMSCompactAtFullCollection開關參數(默認是開啓的),用於CMS收集器頂不住要進行FullGC時開啓內存碎片的合併整理過程,內存整理的過程是沒法併發的,空間碎片問題也就沒有了,但停頓時間不得不變長了。
虛擬機還提供了另一個參數-XX:CMSFullGCsBeforeCompaction,這個參數是用於設置執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的(默認爲0,表示每次進入FullGC時,都進行碎片整理)。
7.G1
G1是一款面向服務端應用的垃圾收集器。與其餘GC收集器相比,G1具有以下特色:
1.併發與並行:G1能充分利用多CPU,多核環境下的硬件優點,使用多個CPU來縮短Stop the World 聽段時間。部分其餘垃圾收集器本來須要停頓JAVA線程執行的GC的動做,G1收集器仍然能夠經過併發的方式讓Java程序繼續執行。