對於GC來講,當程序員建立對象時,GC就開始監控這個對象的地址、大小以及使用狀況。一般,GC採用有向圖的方式記錄和管理堆(heap)中的全部對象。經過這種方式肯定哪些對象是\"可達的\",哪些對象是\"不可達的\".當GC肯定一些對象爲\"不可達\"時,GC就有責任回收這些內存空間。程序員
最基礎的收集算法 —— 標記/清除算法算法
標記/清除算法的基本思想就跟它的名字同樣,分爲「標記」和「清除」兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象。多線程
標記階段:標記的過程其實就是前面介紹的可達性分析算法的過程,遍歷全部的GC Roots對象,對從GC Roots對象可達的對象都打上一個標識,通常是在對象的header中,將其記錄爲可達對象;併發
清除階段:清除的過程是對堆內存進行遍歷,若是發現某個對象沒有被標記爲可達對象(經過讀取對象header信息),則將其回收。jvm
上圖是標記/清除算法的示意圖,在標記階段,從對象GC Root 1能夠訪問到B對象,從B對象又能夠訪問到E對象,所以從GC Root 1到B、E都是可達的,同理,對象F、G、J、K都是可達對象;到了清除階段,全部不可達對象都會被回收。高併發
在垃圾收集器進行GC時,必須中止全部Java執行線程(也稱"Stop The World"),緣由是在標記階段進行可達性分析時,不能夠出現分析過程當中對象引用關係還在不斷變化的狀況,不然的話可達性分析結果的準確性就沒法獲得保證。在等待標記清除結束後,應用線程纔會恢復運行。優化
前面剛提過,後續的收集算法是在標記/清除算法的基礎上進行改進而來的,那也就是說標記/清除算法有它的不足。其實瞭解了它的原理,其缺點也就不難看出了。線程
一、效率問題。標記和清除兩個階段的效率都不高,由於這兩個階段都須要遍歷內存中的對象,不少時候內存中的對象實例數量是很是龐大的,這無疑很耗費時間,並且GC時須要中止應用程序,這會致使很是差的用戶體驗。3d
二、空間問題。標記清除以後會產生大量不連續的內存碎片(從上圖能夠看出),內存空間碎片太多可能會致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾回收動做。對象
複製算法
爲了解決效率問題,複製算法出現了。複製算法的原理是:將可用內存按容量劃分爲大小相等的兩塊,每次使用其中的一塊。當這一塊的內存用完了,就將還存活的對象複製到另外一塊內存上,而後把這一塊內存全部的對象一次性清理掉。用圖說明以下:
回收前:
回收後:
複製算法每次都是對整個半區進行內存回收,這樣就減小了標記對象遍歷的時間,在清除使用區域對象時,不用進行遍歷,直接清空整個區域內存,並且在將存活對象複製到保留區域時也是按地址順序存儲的,這樣就解決了內存碎片的問題,在分配對象內存時不用考慮內存碎片等複雜問題,只須要按順序分配內存便可。
複製算法簡單高效,優化了標記/清除算法的效率低、內存碎片多的問題。可是它的缺點也很明顯:
一、將內存縮小爲原來的一半,浪費了一半的內存空間,代價過高;
二、若是對象的存活率很高,極端一點的狀況假設對象存活率爲100%,那麼咱們須要將全部存活的對象複製一遍,耗費的時間代價也是不可忽視的。
基於以上覆制算法的缺點,因爲新生代中的對象幾乎都是「朝生夕死」的(達到98%),如今的商業虛擬機都採用複製算法來回收新生代。因爲新生代的對象存活率低,因此並不須要按照1:1的比例來劃份內存空間,而是將內存分爲一塊較大的Eden空間和兩塊較小的From Survivor空間、To Survivor空間,三者的比例爲8:1:1。每次使用Eden和From Survivor區域,To Survivor做爲保留空間。GC開始時,對象只會存在於Eden區和From Survivor區,To Survivor區是空的。GC進行時,Eden區中全部存活的對象都會被複制到To Survivor區,而在From Survivor區中,仍存活的對象會根據它們的年齡值決定去向,年齡值達到年齡閥值(默認爲15,新生代中的對象每熬過一輪垃圾回收,年齡值就加1)的對象會被移到老年代中,沒有達到閥值的對象會被複制到To Survivor區。接着清空Eden區和From Survivor區,新生代中存活的對象都在To Survivor區。接着, From Survivor區和To Survivor區會交換它們的角色,也就是新的To Survivor區就是上次GC清空的From Survivor區,新的From Survivor區就是上次GC的To Survivor區,總之,無論怎樣都會保證To Survivor區在一輪GC後是空的。GC時當To Survivor區沒有足夠的空間存放上一次新生代收集下來的存活對象時,須要依賴老年代進行分配擔保,將這些對象存放在老年代中。
標記/整理算法
複製算法在對象存活率較高時要進行較多的複製操做,效率會變得很低,更關鍵的是,若是不想浪費50%的內存空間,就須要有額外的內存空間進行分配擔保,以應對內存中對象100%存活的極端狀況,所以,在老年代中因爲對象的存活率很是高,複製算法就不合適了。根據老年代的特色,高人們提出了另外一種算法:標記/整理算法。從名字上看,這種算法與標記/清除算法很像,事實上,標記/整理算法的標記過程任然與標記/清除算法同樣,但後續步驟不是直接對可回收對象進行回收,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊線之外的內存。
回收前:
回收後:
能夠看到,回收後可回收對象被清理掉了,存活的對象按規則排列存放在內存中。這樣一來,當咱們給新對象分配內存時,jvm只須要持有內存的起始地址便可。標記/整理算法不只彌補了標記/清除算法存在內存碎片的問題,也消除了複製算法內存減半的高額代價,可謂一箭雙鵰。但任何算法都有缺點,就像人無完人,標記/整理算法的缺點就是效率也不高,不只要標記存活對象,還要整理全部存活對象的引用地址,在效率上不如複製算法。
三種算法的比較
效率:複製算法 > 標記/整理算法 > 標記/清除算法(標記/清除算法有內存碎片問題,給大對象分配內存時可能會觸發新一輪垃圾回收)
內存整齊率:複製算法 = 標記/整理算法 > 標記/清除算法
內存利用率:標記/整理算法 = 標記/清除算法 > 複製算法
終極算法 —— 分代收集算法
分代的垃圾回收策略,是基於這樣一個事實:不一樣的對象的生命週期是不同的。所以,不一樣生命週期的對象能夠採起不一樣的回收算法,以便提升回收效率。
年輕代(Young Generation)
1.全部新生成的對象首先都是放在年輕代的。年輕代的目標就是儘量快速的收集掉那些生命週期短的對象。
2.新生代內存按照8:1:1的比例分爲一個eden區和兩個survivor(survivor0,survivor1)區。一個Eden區,兩個 Survivor區(通常而言)。大部分對象在Eden區中生成。回收時先將eden區存活對象複製到一個survivor0區,而後清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor0區存活對象複製到另外一個survivor1區,而後清空eden和這個survivor0區,此時survivor0區是空的,而後將survivor0區和survivor1區交換,即保持survivor1區爲空, 如此往復。
3.當survivor1區不足以存放 eden和survivor0的存活對象時,就將存活對象直接存放到老年代。如果老年代也滿了就會觸發一次Full GC,也就是新生代、老年代都進行回收
4.新生代發生的GC也叫作Minor GC,MinorGC發生頻率比較高(不必定等Eden區滿了才觸發)
年老代(Old Generation)
1.在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。所以,能夠認爲年老代中存放的都是一些生命週期較長的對象。
2.內存比新生代也大不少(大概比例是1:2),當老年代內存滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代對象存活時間比較長,存活率標記高。
持久代(Permanent Generation)
用於存放靜態文件,如Java類、方法等。持久代對垃圾回收沒有顯著影響,可是有些應用可能動態生成或者調用一些class,例如Hibernate 等,在這種時候須要設置一個比較大的持久代空間來存放這些運行過程當中新增的類。
新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge
老年代收集器使用的收集器:Serial Old、Parallel Old、CMS
Serial收集器(複製算法)
新生代單線程收集器,標記和清理都是單線程,優勢是簡單高效。
Serial Old收集器(標記-整理算法)
老年代單線程收集器,Serial收集器的老年代版本。
ParNew收集器(中止-複製算法)
新生代收集器,能夠認爲是Serial收集器的多線程版本,在多核CPU環境下有着比Serial更好的表現。
Parallel Scavenge收集器(中止-複製算法)
並行收集器,追求高吞吐量,高效利用CPU。吞吐量通常爲99%, 吞吐量= 用戶線程時間/(用戶線程時間+GC線程時間)。適合後臺應用等對交互相應要求不高的場景。
Parallel Old收集器(中止-複製算法)
Parallel Scavenge收集器的老年代版本,並行收集器,吞吐量優先
CMS(Concurrent Mark Sweep)收集器(標記-清理算法)
高併發、低停頓,追求最短GC回收停頓時間,cpu佔用比較高,響應時間快,停頓時間短,多核cpu 追求高響應時間的選擇
因爲對象進行了分代處理,所以垃圾回收區域、時間也不同。GC有兩種類型:Scavenge GC和Full GC。
Scavenge GC
通常狀況下,當新對象生成,而且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活對象,而且把尚且存活的對象移動到Survivor區。而後整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。由於大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,因此Eden區的GC會頻繁進行。於是,通常在這裏須要使用速度快、效率高的算法,使Eden去能儘快空閒出來。
Full GC
對整個堆進行整理,包括Young、Tenured和Perm。Full GC由於須要對整個堆進行回收,因此比Scavenge GC要慢,所以應該儘量減小Full GC的次數。在對JVM調優的過程當中,很大一部分工做就是對於FullGC的調節。有以下緣由可能致使Full GC:
1.年老代(Tenured)被寫滿
2.持久代(Perm)被寫滿
3.System.gc()被顯示調用
4.上一次GC以後Heap的各域分配策略動態變化