首先針對(duì)垃圾收集提出的兩個(gè)問(wèn)題?
- 什么時(shí)候回收?
- 怎么回收?
針對(duì)問(wèn)題1,為了回答什么時(shí)候回收這個(gè)問(wèn)題,就需要清楚處于怎樣狀態(tài)下的對(duì)象才需要回收。
處于怎樣狀態(tài)下的對(duì)象才需要回收?在強(qiáng)引用下,只有當(dāng)對(duì)象失去所有引用的時(shí)候,才要對(duì)其進(jìn)行回收。
那么如何判斷對(duì)象處于是否引用的狀態(tài)呢?
目前有兩種主流辦法:
- 引用計(jì)數(shù)法
引用計(jì)數(shù)法是指每個(gè)對(duì)象都有一個(gè)引用計(jì)數(shù)器,每當(dāng)該對(duì)象被引用,計(jì)數(shù)器加1,失去一個(gè)引用,計(jì)數(shù)器減1。如果該對(duì)象的計(jì)數(shù)器值為0時(shí),則說(shuō)明該對(duì)象無(wú)任何引用。
該方法的優(yōu)勢(shì):判定效率高
劣勢(shì):解決不了"循環(huán)引用"問(wèn)題
那么,何為循環(huán)引用呢?
舉個(gè)例子:
//jack所引用的對(duì)象引用計(jì)數(shù)加1,reference = 1
Student jack = new Student();
//lucy所引用的對(duì)象引用計(jì)數(shù)加1,reference = 1
Student lucy = new Student();
//jack.goodFriend所引用的對(duì)象(即為jack所引用的對(duì)象)引用計(jì)數(shù)加1,reference = 2
jack.goodFriend = lucy;
//jack.goodFriend所引用的對(duì)象(即為lucy所引用的對(duì)象)引用計(jì)數(shù)加1,reference = 2
lucy.goodFriend = jack;
//jack.goodFriend所引用的對(duì)象(即為jack所引用的對(duì)象)引用計(jì)數(shù)減1,reference = 1
jack = null;
//jack.goodFriend所引用的對(duì)象(即為lucy所引用的對(duì)象)引用計(jì)數(shù)減1,reference = 1
lucy = null;
** 在該例子中,雖然兩個(gè)對(duì)象還都有引用計(jì)數(shù),但是經(jīng)過(guò) jack = null 和 lucy = null 之后,都無(wú)法再訪問(wèn)這兩個(gè)對(duì)象了。所以由于這個(gè)劣勢(shì),在java中并沒(méi)有使用到它,相反,使用的下面這種垃圾收集搜索算法 **
- 根搜索算法
根搜索算法是指:通過(guò)一系列名為"GC Roots"的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)起開(kāi)始向下搜索,搜索所走過(guò)的路徑成為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連,即該對(duì)象到GC Roots不可達(dá)時(shí),則此對(duì)象已經(jīng)失去所有引用,已不可用了。
那么,哪些對(duì)象可以充當(dāng)GC Roots對(duì)象呢?(為什么呢?)
- 虛擬機(jī)棧(棧幀中的本地變量表)中的引用的對(duì)象
- 方法區(qū)中的類靜態(tài)屬性引用的對(duì)象
- 方法區(qū)中的常量引用的對(duì)象
- 本地方法棧中JNI(Native方法)的引用的對(duì)象
擴(kuò)展,"引用"這個(gè)詞語(yǔ),在Java里面的意思可有很多呢,光我知道就至少有四個(gè),它們分別是:
- 強(qiáng)引用(Strong References)
對(duì)于強(qiáng)引用,則是我們經(jīng)常在程序里面new一個(gè)對(duì)象,即 Boy boy = new Boy(),只要boy不置為null,則我們new的這個(gè)對(duì)象就會(huì)一直存在,垃圾收集器這家伙就不敢拿它怎么辦。
- 軟引用(Soft References)
對(duì)于軟引用的聲明 SoftReference<T> softReference = new SoftReference<T>(Object obj);
具體例子: SoftReference<T> softReference = new SoftReference<T>(new Boy());
這個(gè)時(shí)候該Boy()實(shí)例就持有一個(gè)強(qiáng)引用boy和一個(gè)軟引用softReference。
那么該軟引用softReference有什么用呢?
當(dāng)boy = null 的時(shí)候,此時(shí)可以通過(guò)softReference.get()方法來(lái)重新獲得一個(gè)Boy的強(qiáng)引用。那么我就在想了,為什么要存在這個(gè)軟引用呢?我再用一個(gè)強(qiáng)引用指向Boy不也可以嗎?
先來(lái)看一下軟引用的特點(diǎn):
該軟引用的確可以重新獲得一個(gè)該對(duì)象的強(qiáng)引用,而且該軟引用指向的對(duì)象也不會(huì)被垃圾收集器給收集,但是一旦JVM發(fā)現(xiàn)內(nèi)存不夠,那接下來(lái)就要對(duì)這些軟引用開(kāi)刀了---對(duì)它們所引用的對(duì)象進(jìn)行收集,以獲得所需要的內(nèi)存。而一旦垃圾收集結(jié)束,該softReference.get()方法返回的便是null了。所以,軟引用這種引用可以幫助我們?cè)俅潍@得強(qiáng)引用,但是它也有可能會(huì)被清理掉。
那么這種特點(diǎn)的意義何在?
軟引用指向一個(gè)對(duì)象,一塊內(nèi)存,但是該對(duì)象我們有可能會(huì)使用到它,所以我們需要一個(gè)get()方法來(lái)立即獲得該對(duì)象的一個(gè)強(qiáng)引用,但是也可能不會(huì)使用到它,可這樣的話,這個(gè)對(duì)象又占著一塊內(nèi)存資源,所以我認(rèn)為在這里JVM非常巧妙地采取了一種折中辦法,在內(nèi)存不夠,OutOfMemory的時(shí)候,就要把這個(gè)對(duì)象給回收掉,空出多余的內(nèi)存供系統(tǒng)正常使用。實(shí)在是妙呀!那么在什么應(yīng)用場(chǎng)景下會(huì)使用到軟引用呢?通過(guò)對(duì)軟引用的了解,我認(rèn)為在對(duì)數(shù)據(jù)、資源進(jìn)行緩存的時(shí)候需要用到,有些非必須資源我們可以用一個(gè)軟引用持有,當(dāng)還沒(méi)被回收掉的時(shí)候,可以提升應(yīng)用程序的性能,而當(dāng)內(nèi)存不夠,需要回收的時(shí)候,那就給回收掉,也沒(méi)什么太大的損失。
- 弱引用(Weak References)
弱引用是什么?
聲明一個(gè)弱引用WeakReference<T> weakWidget = new WeakReference<T>(Object obj)
舉個(gè)例子:WeakReference<T> weakReference = new WeakReference<T>(new Boy());
這個(gè)時(shí)候weakReference就是作為一個(gè)指向Boy對(duì)象的弱引用。
那么這個(gè)弱引用有什么特點(diǎn)呢?
在JVM中,如果一個(gè)對(duì)象被一個(gè)弱引用所指向,那么該對(duì)象首先會(huì)在第一次垃圾收集周期被標(biāo)記(沒(méi)有任何條件,直接就會(huì)被標(biāo)記),然后在第二次垃圾收集周期被回收掉。
不過(guò)在JAVA中提供了一個(gè)WeakHashMap()類,根據(jù)名字就可以得知這個(gè)類的一些基本用法了。WeakHashMap()類中的key為弱引用類型,value則為實(shí)例對(duì)象。當(dāng)key所引用的對(duì)象被清理掉之后,該WeakHashMap()則會(huì)自動(dòng)調(diào)用remove()方法來(lái)將對(duì)應(yīng)的一組key-value給刪除掉。
那么弱引用的這種特點(diǎn)有什么作用嗎?
在學(xué)習(xí)這個(gè)弱引用的時(shí)候,查閱了許多的英文資料,不同的資料描述不一樣,但是基本上說(shuō)的還都是同一個(gè)東西。于此同時(shí)又對(duì)比了一下軟引用,個(gè)人認(rèn)為弱引用和它的功能比較類似,也是作為數(shù)據(jù)、資源緩存的一個(gè)很好的API,但之所以弱引用中有一個(gè) "弱"字,就是因?yàn)樵擃愋偷囊貌恍枰魏螚l件,直接就會(huì)被標(biāo)記為垃圾,然后接下來(lái)一步就會(huì)被清理掉。所以在清理之前,我們可以通過(guò) weakReference.get()來(lái)再次獲得一個(gè)該對(duì)象的引用,等到清理掉之后,返回的就是null。
4.虛引用(Phantom References)
虛引用可以理解為該引用指向了一個(gè)已經(jīng)調(diào)用過(guò)一次finalize()方法的對(duì)象,那么再下一次垃圾收集的時(shí)候,就果斷將該對(duì)象給回收掉。虛引用是引用中最弱最弱的一種,以至于調(diào)用get()方法返回值始終都是null。
虛引用的清除過(guò)程大概是怎樣的呢?
Java提供了一個(gè)ReferenceQueue類,即為引用隊(duì)列類,JVM會(huì)將該虛引用入隊(duì)到該ReferenceQueue,等到出隊(duì)的時(shí)候,也就是對(duì)象回收的時(shí)候,與此同時(shí),也會(huì)給系統(tǒng)發(fā)送一個(gè)信號(hào),表示該對(duì)象已被回收。
所以根據(jù)"對(duì)象被回收要接受到信號(hào)"這個(gè)特性,我們便先人一步知道了該對(duì)象被回收的時(shí)間,這個(gè)時(shí)候我們可以做一些后續(xù)的操作。
好了,總結(jié)完了對(duì)象何時(shí)會(huì)被回收之后,接下來(lái)要看看對(duì)象是如何被回收的。提到"如何"二字,如果用編程的思想來(lái)考慮的話,就是設(shè)計(jì)算法的問(wèn)題了。
所以在"如何回收對(duì)象"這個(gè)問(wèn)題上,JVM給我們提供了四種方法來(lái)解決。
- 標(biāo)記-清除(Mark-Sweep)算法

簡(jiǎn)述一下該算法:該算法分為兩個(gè)階段:標(biāo)記 和 清除 階段。
在標(biāo)記階段,JVM所要做的事情有,給待回收的對(duì)象做上標(biāo)記。
在清除階段,JVM則會(huì)命令垃圾收集器在一次垃圾回收的時(shí)候?qū)σ呀?jīng)被標(biāo)記的對(duì)象進(jìn)行大清理。
但是,該算法存在怎樣的問(wèn)題呢?
會(huì)有內(nèi)存碎片的產(chǎn)生
那么產(chǎn)生內(nèi)存碎片有什么危害嗎?
內(nèi)存碎片一旦產(chǎn)生,就意味著我們的一部分內(nèi)存就被分割成一塊一塊較小的內(nèi)存了,這樣每當(dāng)有占有內(nèi)存較大的對(duì)象要來(lái)分配的話,我們沒(méi)有足夠的內(nèi)存來(lái)提供,但是這個(gè)對(duì)象又不可能不給人家分配內(nèi)分對(duì)不對(duì)?所以JVM就不得不再次把垃圾收集器給叫過(guò)來(lái),說(shuō):"看吧,都說(shuō)了不建議你用這種 標(biāo)記-清除方式 方法來(lái)干活,你就是不聽(tīng),看看現(xiàn)在麻煩來(lái)了吧?你趕緊再去收集一次垃圾吧,抓緊騰出一塊地方給剛剛那個(gè)新來(lái)的客人,人家是客,我們可惹不起"。垃圾收集器受到老大的這般訓(xùn)斥后,就趕緊屁顛屁顛地跑過(guò)去干活了。
- 標(biāo)記-整理(Mark-Compact)算法

簡(jiǎn)述一下該算法:該算法與上一次算法的不同之處就在于,當(dāng)每個(gè)待回收的對(duì)象被做上標(biāo)記之后,垃圾收集器先不著急把它們一個(gè)個(gè)地給回收掉,而且先粗中有細(xì)地先把每個(gè)對(duì)象進(jìn)行一個(gè)整理,怎么整理呢?將被標(biāo)記的對(duì)象從第一個(gè)到最后一個(gè)依次有序地重新排列在內(nèi)存的一端,然后再給一鍋端了,這樣做的好處與第一個(gè)算法相比,好處自然是大大的,為什么呢?因?yàn)椴粫?huì)產(chǎn)生內(nèi)存碎片呀!
注意:該算法一般用在老年代內(nèi)存區(qū)
- 復(fù)制(Copying)算法

簡(jiǎn)述一下該算法:"復(fù)制"二字,我們可以大概猜測(cè)這種算法可能是要復(fù)制一塊內(nèi)存吧?沒(méi)錯(cuò),準(zhǔn)確地說(shuō),這種算法它會(huì)將內(nèi)存分為均等的兩份cake1和cake2,每份一模一樣,不多也不少。然后在為對(duì)象分配內(nèi)存的時(shí)候,會(huì)先在cake1上分配。最后當(dāng)cake1上的內(nèi)存被用光,要用到cake2內(nèi)存的時(shí)候,就會(huì)先去cake1上檢測(cè)哪些對(duì)象是可回收的,哪些是不可回收的。對(duì)于暫時(shí)還不可回收的對(duì)象,我們就直接將其依次有序地復(fù)制到cake2上,對(duì)于那些可回收的對(duì)象,就果斷讓垃圾收集器過(guò)來(lái)把它們統(tǒng)統(tǒng)給趕走。這樣一來(lái),我們的cake1就又完全變成一塊嶄新等待開(kāi)發(fā)的內(nèi)存了。這樣每當(dāng)再次需要為對(duì)象分配內(nèi)存的時(shí)候,就在cake2上進(jìn)行,接下來(lái)的過(guò)程就像第一次一樣,循環(huán)交互,協(xié)同工作。
這種算法的優(yōu)點(diǎn):很明顯,這種算法也不會(huì)產(chǎn)生內(nèi)存碎片(其實(shí)只要不是隨意地對(duì)對(duì)象進(jìn)行回收,回收之前或者之后稍微做一些處理,都不會(huì)產(chǎn)生內(nèi)存碎片的),而且實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效。
不過(guò)上面的那一種算法只是剛誕生時(shí)候的設(shè)計(jì),它將內(nèi)存按照1:1的比例來(lái)分配,這樣有時(shí)候會(huì)造成50%的內(nèi)存浪費(fèi),這對(duì)于程序員來(lái)說(shuō)真得很讓人痛心,那么這種算法有沒(méi)有什么改進(jìn)呢?
引用一段來(lái)自周志明先生所著的《深入理解Java虛擬機(jī)——JVM高級(jí)特性與最佳實(shí)踐(第2版)》的原話:
IBM的專門研究表明,新生代中的對(duì)象98%是朝生夕死的,所以并不需要按照1:1的比例來(lái)劃分內(nèi)存空間。而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次需要為對(duì)象分配內(nèi)存的時(shí)候就現(xiàn)在Eden和其中的一塊Survivor。當(dāng)回收時(shí),將Eden和Survivor中還存活著的對(duì)象一次性拷貝到另外一個(gè)Survivor空間上,最后清理掉Eden和剛才用過(guò)的Survivor的空間。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8:1,也就是每次新生代中可用內(nèi)存空間為整個(gè)新生代容量的90%,只有10%的內(nèi)存是會(huì)被"浪費(fèi)"的。
從這段話中我知道了原來(lái)該算法將內(nèi)存劃分比例從1:1調(diào)整到了8:1,其中劃分了三個(gè)區(qū)域:Eden和兩個(gè)Survivor,然后內(nèi)存分配首先在Eden和一塊Surivor上(也就是我的那個(gè)cake1),然后
當(dāng)一次垃圾收集到來(lái)的時(shí)候,會(huì)根據(jù)上邊復(fù)制算法描述的那樣,該轉(zhuǎn)移的轉(zhuǎn)移,該清除的清除。不過(guò)轉(zhuǎn)移的內(nèi)存是第二塊Survivor區(qū)域。
看完這本書(shū)里面的這段描述之后,覺(jué)得豁然開(kāi)朗。但是隨著思維的慣性又思考下去,發(fā)現(xiàn)遇到了一個(gè)問(wèn)題:如果第二塊Survivor的內(nèi)存不夠存儲(chǔ)轉(zhuǎn)移的對(duì)象了該怎么辦?就不存儲(chǔ)內(nèi)存了嗎?或者會(huì)發(fā)生內(nèi)存溢出嗎?又接著看下去發(fā)現(xiàn)原來(lái)書(shū)中對(duì)我這個(gè)疑惑給予了一定的解釋,他是這樣說(shuō)的
當(dāng)然,98%的對(duì)象可回收只是一般場(chǎng)景下的數(shù)據(jù),我們沒(méi)有辦法保證每次回收都只有不多余10%的對(duì)象存活,當(dāng)Survivor空間不夠用時(shí),需要依賴其他內(nèi)存(這里指老年代)進(jìn)行分配擔(dān)保(Handle Promotion)。
有了這段話的解釋,就多多少少解決了一些我的疑惑。不過(guò)又發(fā)現(xiàn)這兩段引用中,有兩個(gè)詞不是太理解,一個(gè)叫"新生代",一個(gè)叫"老年代"。這兩個(gè)**代指的又是什么呢?
4.分代收集(Generational Collection)算法
簡(jiǎn)述一下分代收集算法:"分代"是指根據(jù)對(duì)象的存活周期的不同把內(nèi)存劃分為幾塊,一般是把java堆分為新生代和老年代。哈哈,這里終于提到了"新生代"和"老年代"啦!那么,它倆具體指什么呢?
看一下這本書(shū)對(duì)其的簡(jiǎn)單介紹:
在新生代中,每次垃圾收集時(shí)都會(huì)有大批對(duì)象死去,只有少量存活。那就選用復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集。而老年代中因?yàn)閷?duì)象存活率高,沒(méi)有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用"標(biāo)記-清理"或"標(biāo)記-整理"算法來(lái)進(jìn)行回收。
真好,不光介紹了這兩個(gè)代,而且把我剛剛學(xué)過(guò)的那些算法也用在相應(yīng)了代上了。根據(jù)復(fù)制算法的特點(diǎn)我知道了何時(shí)選擇它,當(dāng)大部分對(duì)象處于"朝生夕死",使用次數(shù)不多的時(shí)候,這種算法就突顯出了它的優(yōu)勢(shì):內(nèi)存浪費(fèi)少,不會(huì)產(chǎn)生內(nèi)存碎片,并且實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效。而當(dāng)對(duì)象存活率高的時(shí)候,就用那兩個(gè)標(biāo)記算法。
垃圾收集器
前邊學(xué)習(xí)了垃圾收集都有哪些算法,那么接下來(lái)就要來(lái)了解一下運(yùn)行這些垃圾收集算法的東西,那就是垃圾收集器。
垃圾收集器的分類

Serial收集器
Serial收集器是最基本、歷史最悠久的收集器,在JDK1.3.1之前是虛擬機(jī)新生代收集的唯一選擇。
特點(diǎn):?jiǎn)尉€程,簡(jiǎn)單高效(因?yàn)闆](méi)有線程交互所帶來(lái)的系統(tǒng)開(kāi)銷),進(jìn)行垃圾收集時(shí)需要暫停其他所有線程(stop the world),所以就會(huì)有一定的卡頓現(xiàn)象。

應(yīng)用:虛擬機(jī)在Clinet模式下的默認(rèn)新生代收集器。
ParNew收集器
ParNew收集器是Serial收集器的多線程版本。
特點(diǎn):多線程,速度相對(duì)較慢(因?yàn)橛芯€程交互所帶來(lái)的系統(tǒng)開(kāi)銷),進(jìn)行垃圾收集時(shí)需要暫停其他所有進(jìn)程

應(yīng)用:虛擬機(jī)在Server模式下首選的新生代收集器,不過(guò)為什么呢?目前除了Serial收集器外,目前只有它能與CMS收集器配合工作。
Parallel Scavenge收集器
Parallel Scavenge收集器是一個(gè)多線程的新生代收集器,使用復(fù)制算法。
特點(diǎn):多線程,吞吐量?jī)?yōu)先
所謂吞吐量是指:CPU運(yùn)行用戶代碼的時(shí)間與CPU總消耗時(shí)間的比值,即吞吐量 = 運(yùn)行用戶代碼時(shí)間 / (運(yùn)行用戶代碼時(shí)間 + 垃圾收集時(shí)間)
Serial Old收集器
Serial Old收集器是Serial收集器的單線程老年代版本,使用"標(biāo)記-整理"算法
特點(diǎn):適用于老年代,單線程

應(yīng)用:
虛擬機(jī)在Client模式下使用該收集器;
在Server模式下,在JDK1.5及之前的版本中與Parallel Scavenge收集器搭配使用作為CMD收集器的后備預(yù)案,在并發(fā)收集發(fā)生Concurrent Mode Failure的時(shí)候使用。
Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和"標(biāo)記-整理"算法。
特點(diǎn):適用于老年代,多線程

應(yīng)用:在注重吞吐量及CPU資源敏感的場(chǎng)合,優(yōu)先考慮Parallel Scavenge收集器和Parallel Old收集器
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器,基于"標(biāo)記-清除"算法。
收集過(guò)程:
1.初始標(biāo)記(CMS initial mark)
特點(diǎn):?jiǎn)尉€程,stop the world
作用:僅僅是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,速度很快
2.并發(fā)標(biāo)記(CMS concurrent mark)
特點(diǎn):?jiǎn)尉€程,與其他線程并發(fā)運(yùn)行
3.重新標(biāo)記(CMS remark)
特點(diǎn):多線程,stop the world
作用:修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運(yùn)行而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄。
4.并發(fā)清除(CMS concurrent sweep)
特點(diǎn):?jiǎn)尉€程,與其他線程并發(fā)運(yùn)行

應(yīng)用:服務(wù)端
很明顯的缺點(diǎn):
1.對(duì)CPU資源敏感。CMS默認(rèn)啟動(dòng)的回收線程(CPU數(shù)量 + 3) / 4 ,當(dāng)CPU >= 4的時(shí)候,并發(fā)回收時(shí)垃圾收集線程最多占用不超過(guò)25%的CPU資源,但是當(dāng)CPU < 4 的時(shí)候,CMS對(duì)用戶程序的影響就可能變得很大。
2.CMS無(wú)法處理"浮動(dòng)垃圾"(Floating Garbage),可能出現(xiàn)"Concurrent Mode Failure"失敗而導(dǎo)致另一次Full GC的產(chǎn)生。 什么是"浮動(dòng)垃圾"呢?在CMS結(jié)束標(biāo)記之后,有一部分對(duì)象也成可以被清理的垃圾了,可CMS無(wú)法在本次的垃圾處理過(guò)程中回收掉它們,所以又動(dòng)態(tài)產(chǎn)生的這部分垃圾叫做"浮動(dòng)垃圾"。在CMS運(yùn)行的同時(shí),也有用戶線程在運(yùn)行,所以就需要預(yù)留夠足夠的內(nèi)存空間給用戶線程,而當(dāng)CMS不能保證這一點(diǎn)的時(shí)候,就會(huì)出現(xiàn)"Concurrent Mode Failure"這種錯(cuò)誤。
3.CMS基于的"標(biāo)記-清除"算法會(huì)產(chǎn)生內(nèi)存碎片。(不過(guò)CMS較好地解決了這種問(wèn)題,解決的辦法便是在經(jīng)過(guò)一次的CMS垃圾處理過(guò)程服務(wù)之后,還會(huì)再送一個(gè)碎片整理服務(wù))
G1收集器
G1收集器是當(dāng)前收集器技術(shù)發(fā)展的最前沿成果,基于"標(biāo)記-整理"算法,
特點(diǎn):能夠精確地控制停頓,可以實(shí)現(xiàn)在基本不犧牲吞吐量的前提下完成低停頓的內(nèi)存回收。
那么,為什么有以上優(yōu)點(diǎn)呢?引用一段來(lái)自《深入理解Java虛擬機(jī)-JVM高級(jí)特性與最佳實(shí)踐》
G1收集器極力地避免全區(qū)域的垃圾收集,之前的收集器進(jìn)行收集的范圍都是整個(gè)新生代或老年代,而G1將整個(gè)JAVA堆(包括新生代、老年代)劃分為多個(gè)大小固定的獨(dú)立區(qū)域(Region),并且跟蹤這些區(qū)域里面的垃圾堆積程度,在后臺(tái)維護(hù)一個(gè)優(yōu)先列表,每次根據(jù)允許的收集時(shí)間,優(yōu)先回收垃圾最多的區(qū)域(Garbage First名字的由來(lái))。這樣一來(lái),區(qū)域劃分以及有優(yōu)先級(jí)的區(qū)域回收,保證了G1收集器在有限的時(shí)間內(nèi)可以獲得最高的收集效率
內(nèi)存分配與回收策略
1.對(duì)象優(yōu)先在Eden分配
大多數(shù)情況下,對(duì)象在新生代Eden區(qū)中分配。當(dāng)Eden區(qū)沒(méi)有足夠的空間進(jìn)行分配時(shí),虛擬機(jī)將發(fā)起一次Minor Gc(新生代垃圾收集,復(fù)制算法)。
2.大對(duì)象直接進(jìn)入老年代
1.什么是大對(duì)象?
需要大量連續(xù)內(nèi)存空間的Java對(duì)象(很長(zhǎng)的字符串和數(shù)組)
那么,這樣的大對(duì)象為什么要直接進(jìn)入老年代呢?
因?yàn)榻?jīng)常出現(xiàn)大對(duì)象容易導(dǎo)致內(nèi)存還有不少空間時(shí)就提前觸發(fā)垃圾收集以獲取足夠的連續(xù)空間來(lái)"安置"它們。所以,虛擬機(jī)提供了一個(gè)-XX:PretenureSizeThreshold參數(shù),如果所需內(nèi)存值超過(guò)該參數(shù)值,就直接在老年代中分配,這樣就直接避免了新生代區(qū)頻繁地進(jìn)行GC操作了。
3.長(zhǎng)期存活的對(duì)象將進(jìn)入老年代
如何衡量一個(gè)對(duì)象的存活時(shí)間呢?
JVM為每個(gè)對(duì)象設(shè)置了一個(gè)對(duì)象年齡計(jì)數(shù)器,每一次進(jìn)行分代收集之后,如果位于新生代的對(duì)象還沒(méi)有被收集的話,該年齡計(jì)數(shù)器加1,如果該值超過(guò)一個(gè)閥值(默認(rèn)為15歲),則該對(duì)象會(huì)被調(diào)入到老年代中去享福咯!
4.動(dòng)態(tài)對(duì)象年齡判定
這是另一種可以進(jìn)入老年代的途徑:如果在Survivor空間中相同年齡所有對(duì)象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對(duì)象就可以直接進(jìn)入老年代,而無(wú)須等待到當(dāng)初設(shè)定的那個(gè)閥值。
5.空間分配擔(dān)保
JVM將內(nèi)存分為新生代和老年代,在新生代又分為一個(gè)Eden區(qū)和兩個(gè)Survivor區(qū)域,在進(jìn)行垃圾收集的時(shí)候,第二塊Survivor區(qū)域用于存儲(chǔ)還存活的對(duì)象,但是有可能會(huì)存在所有存活對(duì)象所占內(nèi)存過(guò)多,導(dǎo)致Survivor區(qū)域不夠用,這個(gè)時(shí)候就要向老年代區(qū)域申請(qǐng)擔(dān)保,把多余的對(duì)象放在老年區(qū)。不過(guò)此時(shí)需要有一個(gè)對(duì)老年區(qū)是否也能存放得下這些對(duì)象的一個(gè)評(píng)估,那就是根據(jù)之前每次晉升到老年代的平均大小是否大于老年代的剩余空間大小,
如果大于的話,就意味著此次的對(duì)象有很大的可能性是晉升不到老年代區(qū)的,意思就是老年代內(nèi)存有很大可能是不夠用的,那么該怎么做呢?只能把老年代區(qū)進(jìn)行一次Full GC 來(lái)騰出一些空間了。
但是如果平均大小小于剩余空間的話,那就意味著有很大可能性是能夠晉升的,那么就趕緊把這些對(duì)象給放進(jìn)老年代區(qū)嗎?等等!這里還有一個(gè)HandlePromotionFailure設(shè)置選項(xiàng),該選項(xiàng)的意思是是否允許擔(dān)保失敗(這里是有可能失敗的)。如果允許,一旦老年代區(qū)放不下,那就立馬在新生代區(qū)執(zhí)行MinorGC垃圾收集過(guò)程。如果不允許的話,一旦老年代放不下,那就要在老年代立馬進(jìn)行一次Full GC垃圾收集了。這樣,無(wú)論哪種情況發(fā)生,我們要么在新生代進(jìn)行MinorGC或者在老年代進(jìn)行FullGC,這樣總能盡最大可能來(lái)為對(duì)象分配內(nèi)存空間。
參考資料
書(shū)籍:《深入理解Java虛擬機(jī)-JVM高級(jí)特性與最佳實(shí)踐》周志強(qiáng)