垃圾收集(Garbage Collection ,GC)害晦,是一個(gè)長(zhǎng)久以來(lái)就被思考的問題设拟,當(dāng)考慮GC的時(shí)候蛮瞄,我們必須思考3件事情:
- 哪些內(nèi)存需要回收置济?
- 什么時(shí)候回收解恰?
- 如何回收?
那么在Java中浙于,我們要怎么來(lái)考慮GC呢护盈?首先回想以下內(nèi)存區(qū)域的劃分,其中程序計(jì)數(shù)器羞酗、本地方法棧腐宋、虛擬機(jī)棧三個(gè)區(qū)域隨線程而生,隨線程釋放,棧中的棧幀隨著方法的進(jìn)入和退出執(zhí)行著出棧和入棧的操作胸竞,每一個(gè)棧幀分配多少內(nèi)存基本是在類結(jié)構(gòu)確定時(shí)就已經(jīng)固定的(可能會(huì)進(jìn)行一些優(yōu)化欺嗤,但是大體上已知),因此這幾個(gè)區(qū)域就不需要考慮回收的問題卫枝,因?yàn)榉椒ńY(jié)束或者線程結(jié)束時(shí)煎饼,內(nèi)存自然都被回收。不需要額外的GC算法等校赤。
然而Java堆和方法區(qū)則不一樣吆玖,一個(gè)接口所對(duì)應(yīng)的多個(gè)實(shí)現(xiàn)類所需要的內(nèi)存可能不一樣,一個(gè)方法中的多個(gè)分支所需要的內(nèi)存也可能不一樣痒谴,我們只有在程序處于運(yùn)行期間才能知道程序需要?jiǎng)?chuàng)建那些對(duì)象衰伯,這部分的內(nèi)存的分配和回收是動(dòng)態(tài)的,因此积蔚,垃圾收集器關(guān)注的是這方面的內(nèi)存意鲸。
一. 如何確定對(duì)象可以回收
1.引用計(jì)數(shù)算法
最容易想到與理解的算法,即對(duì)于每一個(gè)對(duì)象尽爆,每當(dāng)該對(duì)象被引用時(shí)怎顾,計(jì)數(shù)器值就+1,引用失效時(shí)漱贱,計(jì)數(shù)器就-1槐雾。因此,當(dāng)對(duì)象的引用計(jì)數(shù)為0時(shí)幅狮,即為不可再被使用的募强。該算法也在一些領(lǐng)域被使用來(lái)進(jìn)行內(nèi)存管理,但是JAVA虛擬機(jī)中并沒有選用該算法崇摄。主要是因?yàn)椴荒芎芎玫慕鉀Q循環(huán)引用的問題擎值。
舉個(gè)簡(jiǎn)單的例子來(lái)說(shuō)明循環(huán)引用:
class Container{ public Object obj ;
}public class ReferTest { public static void main(String[] args){
Container c1 =new Container();
Container c2 =new Container();
c1.obj = c2 ;
c2.obj = c1 ;
c1 = null ;
c2 = null ; //此時(shí)c1 c1會(huì)被判定為死亡對(duì)象么? }
}
事實(shí)上會(huì)被判定為死亡對(duì)象逐抑,因?yàn)镴AVA虛擬機(jī)不是采用引用計(jì)數(shù)來(lái)進(jìn)行判斷的鸠儿,因此如果發(fā)生垃圾回收,c1厕氨,c2 都會(huì)被回收內(nèi)存进每。
2.可達(dá)性分析
Java、C#的主流實(shí)現(xiàn)都是采用該種方式命斧,來(lái)判斷對(duì)象是否存活田晚。
這個(gè)算法的基本思路就是一系列“GC Roots”作為起始點(diǎn),從這些節(jié)點(diǎn)向下搜索国葬,搜索到的所有引用鏈中的對(duì)象都是可達(dá)的肉瓦,其余的對(duì)象都是不可達(dá)的遭京,如上例,即使c1,c2互相引用泞莉,但是c1,c2都不屬于GC Roots對(duì)象,因此都不可達(dá)船殉。
Java中鲫趁,以下幾種對(duì)象可以作為GC Roots:
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象。
- 本地方法棧JNI方法引用的對(duì)象利虫。
- 方法區(qū)類的靜態(tài)屬性引用的對(duì)象挨厚。
- 方法區(qū)常量引用的對(duì)象。
3.引用的分類
了解了GC Roots之后糠惫,我們可能會(huì)希望存在這么一種對(duì)象疫剃,內(nèi)存夠的時(shí)候不進(jìn)行回收,當(dāng)需要內(nèi)存時(shí)再將其回收硼讽。JDK 1.2 中對(duì)引用進(jìn)行了擴(kuò)充巢价。將引用分為了4種,從強(qiáng)到弱依次為;
強(qiáng)引用(Strong Reference)
我們一般情況下使用的都是強(qiáng)引用固阁,如Object o = new Object()壤躲,之類的代碼。只要強(qiáng)引用還在备燃,垃圾收集器就永遠(yuǎn)不會(huì)回收被引用的對(duì)象碉克。
軟引用(Soft Reference)
SoftReference類來(lái)實(shí)現(xiàn),用來(lái)描述一些還有用但是不必須的對(duì)象并齐,在系統(tǒng)如果不回收就會(huì)發(fā)生OOM時(shí)才會(huì)對(duì)軟引用進(jìn)行內(nèi)存回收漏麦。
弱引用(Weak Reference)
WeakReference類來(lái)實(shí)現(xiàn),描述非必需的對(duì)象况褪,強(qiáng)度弱撕贞,只能活到下一次發(fā)生垃圾回收前,無(wú)論那時(shí)內(nèi)存是否短缺窝剖,都會(huì)對(duì)軟引用對(duì)象進(jìn)行內(nèi)存回收
虛引用(Phantom Reference)
PhantomReference類實(shí)現(xiàn)麻掸,不會(huì)對(duì)生存時(shí)間發(fā)生任何影響,唯一目的時(shí)能在這個(gè)對(duì)象被收集器回收時(shí)得到一個(gè)通知赐纱。
4.其他
及其不建議使用finalize()方法,雖然可以在回收時(shí)被調(diào)用脊奋,但是finalize()方法的執(zhí)行代價(jià)高昂,不確定性大疙描,無(wú)法保證各個(gè)對(duì)象的調(diào)用順序诚隙。使用finalize()能做的工作,使用try()finally()或其他方式可以執(zhí)行的更好起胰。大家可以忘記JAVA中有這個(gè)方法的存在久又。本身就是在JAVA剛誕生時(shí)向C/C++程序員做的妥協(xié)巫延,但是未得到優(yōu)化。
方法區(qū)(永久代)進(jìn)行GC的效率極低地消,花費(fèi)較大炉峰,但是在大量使用反射、動(dòng)態(tài)代理等場(chǎng)景都需要虛擬機(jī)具備類卸載的功能脉执,以保證永生代的空間疼阔。
二.垃圾收集算法
1.標(biāo)記清除算法(Mark-Sweep)
算法分為兩個(gè)階段,標(biāo)記與清除半夷。
標(biāo)記階段:標(biāo)記出所有需要回收的對(duì)象婆廊。回收階段:將所有標(biāo)記區(qū)域回收巫橄。由于該算法不對(duì)空間進(jìn)行整理淘邻,因此會(huì)產(chǎn)生大量的內(nèi)存碎片,內(nèi)存空間碎片過多會(huì)導(dǎo)致在分配較大的對(duì)象時(shí)湘换,因?yàn)闆]有連續(xù)的內(nèi)存而不得不提前觸發(fā)一個(gè)GC宾舅。另外,標(biāo)記與清除的過程效率都不高枚尼。這也是最基礎(chǔ)的GC算法贴浙。
2.復(fù)制算法(Copying)
將內(nèi)存的總?cè)萘糠譃閮蓧K,每次只使用其中的一塊署恍,當(dāng)這一塊用完了崎溃,觸發(fā)GC,此時(shí)將還存活的對(duì)象轉(zhuǎn)移到另一塊內(nèi)存中盯质,之前使用的那一塊內(nèi)存完全清理掉袁串。這樣每次對(duì)一個(gè)半?yún)^(qū)進(jìn)行回收,也不會(huì)存在內(nèi)存碎片呼巷,實(shí)現(xiàn)簡(jiǎn)單囱修,運(yùn)行高效,但是一次只能使用半塊內(nèi)存可能會(huì)造成浪費(fèi)王悍。
在新生代中破镰,絕大部分的對(duì)象時(shí)“朝生夕死”的,因此压储,不需要按照1:1來(lái)劃分空間鲜漩。而是將內(nèi)存分為一塊較大的Eden區(qū)以及兩個(gè)Survivor區(qū),HotSpot虛擬機(jī)中集惋,Eden:Survivor=8:1 孕似,每次使用一個(gè)Eden區(qū)以及一個(gè)Survivor區(qū),90%的空間刮刑,觸發(fā)GC后喉祭,將剩余的對(duì)象轉(zhuǎn)移到未使用的Survivor中养渴,然后清理Eden區(qū)和用過的Survivor區(qū),空間不夠時(shí)泛烙,會(huì)擔(dān)保分配到老年代理卑。這樣一次可以使用90%的內(nèi)存空間,極大的提高了內(nèi)存的使用率胶惰。因此傻工,新生代一般采用這種算法來(lái)回收。
3.標(biāo)記整理算法(Mark-Compact)
如果回收時(shí)空間內(nèi)的對(duì)象存活率較高孵滞,那么使用復(fù)制算法一次只能使用50%的空間(以應(yīng)對(duì)所有對(duì)象都存活的情況),因此老年代采用標(biāo)記整理算法鸯匹。先對(duì)需要清理的對(duì)象進(jìn)行標(biāo)記坊饶,然后將存活的對(duì)象都向一端移動(dòng),直接清理掉端邊界以外的內(nèi)存殴蓬。這種方式也不會(huì)留下內(nèi)存碎片匿级。
標(biāo)記整理算法沒有復(fù)制算法快。
三. Java垃圾收集器
(了解即可染厅,需要時(shí)可以網(wǎng)上細(xì)查)
新生代收集器:Serial收集器痘绎、ParNew收集器(Serial的多線程版本)、Parallel Scanvenge收集器(控制吞吐量肖粮,提高相應(yīng)速度)
老年代收集器:Serial Old收集器孤页、Parallel Old收集器、CMS收集器(最短停頓)涩馆、G1(新生代行施、老年代都可回收)
四. 內(nèi)存的分配與回收
新生代:即復(fù)制算法中提到的Eden區(qū)以及2個(gè)Survivor區(qū)。
老年代:新生代存活足夠長(zhǎng)時(shí)間后進(jìn)入老年代魂那。堆上的另一塊區(qū)域蛾号。
Minor GC:發(fā)生在新生代的垃圾收集動(dòng)作。因?yàn)镴ava對(duì)象存活時(shí)間一般較短涯雅,故Minor GC非常頻繁鲜结,一般回收速度也較快。
Full GC:發(fā)生在老年代的垃圾收集動(dòng)作活逆,伴隨著最少一次的Minor GC精刷,且速度較慢(比Minor GC慢10倍以上)
1.空間的分配
1)對(duì)象優(yōu)先在新生代Eden區(qū)分配。當(dāng)Eden區(qū)沒有足夠空間時(shí)划乖,將發(fā)動(dòng)一次Minor GC.
2)較大對(duì)象需要連續(xù)的空間贬养,如長(zhǎng)字符串或數(shù)組,如果放在新生代會(huì)提前觸發(fā)GC琴庵。故大對(duì)象直接進(jìn)入老年代區(qū)域误算,避免頻繁的GC仰美。
3)長(zhǎng)期存活的對(duì)象進(jìn)入老年代,每個(gè)對(duì)象有一個(gè)年齡儿礼,在對(duì)象頭Mark Word中記錄咖杂,剛被創(chuàng)建時(shí)年齡為0,當(dāng)它活過一次Minor GC蚊夫,并且轉(zhuǎn)移到Survivor中诉字,年齡變?yōu)?,此后,在Survivor區(qū)中每活過一個(gè)Minor GC,年齡就會(huì)+1悦污,當(dāng)年齡達(dá)到某個(gè)程度(默認(rèn)為15)践樱,就會(huì)晉升到老年代。
4)此外,為了適應(yīng)內(nèi)存的復(fù)雜情況,年齡不一定達(dá)到規(guī)定值才能進(jìn)入老年代。當(dāng)Survivor區(qū)的相同年齡所有對(duì)象大小大于Survivor區(qū)大小的一半時(shí)冲杀,此年齡就會(huì)被作為判定標(biāo)準(zhǔn),大于等于該年齡的都會(huì)進(jìn)入老年代睹酌。
2.空間的回收--GC
這里我用一張圖來(lái)徹底解釋清除:
需要解釋的地方有:擔(dān)保失敗权谁,這個(gè)的作用在圖上已經(jīng)解釋的很清楚了,可以在JVM參數(shù)設(shè)置憋沿。
另外一個(gè)地方就是平均大小來(lái)作比較旺芽,因?yàn)橛卸嗌賹?duì)象晉升到老年代是無(wú)法知道的,所以只好取之前每一次晉升到老年代的對(duì)象的容量的平均值大小來(lái)作為經(jīng)驗(yàn)值卤妒,來(lái)決定是否進(jìn)行Full GC來(lái)讓老年代騰出更多空間甥绿。如果仍然失敗,那么只能進(jìn)行一次Full GC则披。在我個(gè)人開來(lái)共缕,之所以使用擔(dān)保,經(jīng)驗(yàn)值來(lái)盡可能的只進(jìn)行MinorGC士复,所有的一切图谷,都是為了盡可能不執(zhí)行Full GC的情況下將需要申請(qǐng)的內(nèi)存空間搞定。