本文主要內(nèi)容
- 對(duì)象已死
- 引用
- 垃圾收集算法
- 垃圾收集器
本文主要從概念上介紹內(nèi)存回收及垃圾收集器相關(guān)內(nèi)容,不涉及具體性能調(diào)優(yōu)。
內(nèi)存回收是程序員永恒的主題,雖然Java虛擬機(jī)自動(dòng)回收內(nèi)存,但仍存在內(nèi)存漏泄的可能,需要理解內(nèi)存回收機(jī)制,有助于程序員規(guī)避、排查內(nèi)存泄漏問(wèn)題。
GC機(jī)制,最重要的是三個(gè)問(wèn)題:
- 哪些內(nèi)存需要回收
- 什么時(shí)候回收
- 如何回收
對(duì)象已死
對(duì)象是否已經(jīng)死亡,可被回收,經(jīng)常能聽到下面這種說(shuō)法:
引用計(jì)數(shù)法:給對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用時(shí),計(jì)數(shù)器就加1,當(dāng)引用失效時(shí),計(jì)數(shù)器值就減1,任何時(shí)候計(jì)數(shù)器為0的對(duì)象就是不可能再被使用的
不過(guò),它存在一個(gè)致命缺陷,很難解決循環(huán)引用問(wèn)題,比如對(duì)象AB,A引用B,B也引用A,但它們?cè)贈(zèng)]有被其它人所引用,AB理應(yīng)是被回收對(duì)象,但它們的引用計(jì)數(shù)器仍然不為0,導(dǎo)致無(wú)法回收。
Java虛擬機(jī)不使用此算法來(lái)判定對(duì)象是否死亡,不過(guò)它依然有很多優(yōu)點(diǎn),簡(jiǎn)單。Android 中的智能指針即是使用這種方法,不過(guò)添加了智能指針強(qiáng)弱引用來(lái)解決循環(huán)引用問(wèn)題
可達(dá)性分析算法,這個(gè)算法的基本思路就是通過(guò)一系列的名為“GC Roots"的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過(guò)的路徑稱為引用鏈,當(dāng)一個(gè)對(duì)象到“GC Roots"沒(méi)有任何引用鏈相連,則此對(duì)象不可用,將被回收
在Java中,可作為“GC Roots"的對(duì)象包括以下幾種:
- 虛擬機(jī)棧(棧楨中的本地變量表)中引用的對(duì)象
- 方法區(qū)中的類靜態(tài)屬性引用的對(duì)象
- 方法區(qū)中的常量引用的對(duì)象
- 本地方法棧中JNI(即一般說(shuō)的native方法)的引用的對(duì)象
引用
在JDK1.2以前,引用的概念為:
如果reference類型的數(shù)據(jù)中存儲(chǔ)的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱這塊內(nèi)存代表著一個(gè)引用
JDK1.2之后,引用概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用、軟引用、弱引用、虛引用四種,四種引用強(qiáng)度依次減弱。
強(qiáng)引用,就是指代碼中普遍存在的,類似”O(jiān)bject ojb = new Object()“這類引用,只要強(qiáng)引用還存在,垃圾收集器永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象
軟引用,用來(lái)描述一些還有用,但并非必要的對(duì)象。對(duì)于軟引用關(guān)聯(lián)著的對(duì)象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會(huì)把這些對(duì)象列進(jìn)回收范圍之中并進(jìn)行第二次回收,如果這次回收還是沒(méi)有足夠內(nèi)存,才會(huì)拋出內(nèi)存溢出異常,JDK中提供SoftReference類來(lái)實(shí)現(xiàn)軟引用
弱引用,也是用于描述非必需對(duì)象的,但是它的強(qiáng)度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生之前,當(dāng)垃圾收集器工作時(shí),無(wú)論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉被弱引用關(guān)聯(lián)的對(duì)象,JDK中提供WeakReference來(lái)實(shí)現(xiàn)弱引用
虛引用,它是最弱的一種引用關(guān)系,一個(gè)對(duì)象是否有虛引用的存在,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無(wú)法通過(guò)虛引用取得一個(gè)對(duì)象實(shí)例,為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是希望能在這個(gè)對(duì)象被回收時(shí)收到一個(gè)系統(tǒng)通知。JDK使用PhantomReference來(lái)實(shí)現(xiàn)虛引用
垃圾收集算法
本章將介紹三種垃圾收集算法。
標(biāo)記清除算法,算法分成“標(biāo)記”和“清除”兩個(gè)階段,首先標(biāo)記出需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收掉所有被標(biāo)記的對(duì)象
它主要有兩個(gè)問(wèn)題,一是效率不高,標(biāo)記和清除過(guò)程的效率都不高,另一個(gè)是空間問(wèn)題,標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多,當(dāng)程序在運(yùn)行中需要分配較大對(duì)象,因?yàn)樗槠^(guò)多,可能無(wú)法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作

復(fù)制算法,為了解決效率問(wèn)題,一種稱為復(fù)制的收集算法出現(xiàn)了。它將內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊,當(dāng)這一塊內(nèi)存使用完了,就將還存活著的對(duì)象復(fù)制到另一塊上面,然后再把已使用過(guò)的內(nèi)存空間一次清理掉。每次都是只對(duì)其中的一塊進(jìn)行內(nèi)存回收,內(nèi)存分配時(shí)也就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動(dòng)堆頂指針,按順序分配內(nèi)存即可,實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效。只是代價(jià)高昂,將內(nèi)存縮小為原來(lái)的一半。

現(xiàn)在的商業(yè)虛擬機(jī)都采用這種算法來(lái)回收新生代,新生代中的對(duì)象98%是朝生夕死的,所以并不需要按照1:1的比例來(lái)劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中的一塊Survivor空間,當(dāng)回收時(shí),將Eden和Survivor看還存活的對(duì)象一次性拷貝到另外一塊Survivor空間上,最后清理掉Eden和剛才用過(guò)的Survivor空間。
虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8比1,所以只有10%的空間會(huì)被浪費(fèi)。
如果存活的對(duì)象較多而Survivor空間不夠用時(shí),需要依賴其它內(nèi)存(老年代)進(jìn)行分配擔(dān)保,如果另外一塊Survivor空間沒(méi)有足夠的空間來(lái)存放上一次新生代的存活對(duì)象,這些對(duì)象將直接通過(guò)分配擔(dān)保機(jī)制進(jìn)入老年代
標(biāo)記整理算法,復(fù)制收集算法在對(duì)象存活率較高時(shí)就要執(zhí)行較多的復(fù)制操作,效率將會(huì)變得更低,更關(guān)鍵的是,如果不想浪費(fèi)一半的空間,就需要額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對(duì)所有對(duì)象百分百存活的極端情況。所以老年代一般不直接使用這種算法
根據(jù)老年代的特點(diǎn),提出了“標(biāo)記整理算法”,過(guò)程依然和“標(biāo)記清除算法”一致,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清除,而是讓所有存活對(duì)象都向一端移動(dòng),然后直接清理掉邊界以外的內(nèi)存。

分代收集算法
當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用分代收集算法。它根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊。

一般是把Java堆分成新生代和老年代,新生代又分成一塊較大的Eden和兩塊Survivor。根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ā?/p>
在新生代中,每次垃圾回收時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集。而老年代中因?yàn)閷?duì)象存活率羅高,沒(méi)有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用標(biāo)記清理或者標(biāo)記整理算法。
垃圾收集器
Serial收集器
Serial收集器是最基本、歷史最悠久的收集器。顧名思義,這個(gè)收集器是一個(gè)單線程收集器。單線程的意義并不僅僅說(shuō)明它只會(huì)使用一個(gè)CPU或者一條線程去完成垃圾收集工作,更重要的是它在垃圾收集時(shí),必須暫停其他所有的工作線程(Sun將這件事情稱之為“Stop the world”),直到它結(jié)束
“Stop the world”,非常影響用戶體驗(yàn),虛擬機(jī)在后臺(tái)自動(dòng)發(fā)起和完成的,在用戶不可見(jiàn)的情況下把用戶的正常工作的線程全部停掉。

雖然Serial收集器出現(xiàn)時(shí)間較長(zhǎng),但它依然是Client模式下的默認(rèn)新生代收集器
ParNew收集器
ParNew收集器其實(shí)就是Serial收集器的多線程控制版本,除了使用多條線程進(jìn)行垃圾收集之外,其它和Serial收集器完全一樣。

ParNew收集器是Server模式下虛擬機(jī)中的首選的新生代收集器。而且在單CPU環(huán)境下,ParNew絕對(duì)不會(huì)比Serial更高效,甚至由于存在線程交互的開銷,在兩個(gè)CPU的環(huán)境中都不一定比Serial更好
Parallel Scavenge收集器
Parallel Scavenge收集器也是一個(gè)新生代收集器,它也是使用復(fù)制算法,又是并行的多線程收集器,看上去和ParNew一樣,但它非常的有特點(diǎn)。
吞吐量,CPU用于運(yùn)行用戶代碼的時(shí)間與CPU總消耗時(shí)間的比值,即吞吐量 = 運(yùn)行用戶代碼時(shí)間 / (運(yùn)行用戶代碼時(shí)間 + 垃圾收集時(shí)間)
Parallel Scavenge收集器關(guān)注點(diǎn)即是吞吐量,CMS等收集器的關(guān)注點(diǎn)是盡量縮短垃圾收集時(shí)用戶線程的停頓時(shí)間,而Parallel Scavenge的目的則是達(dá)到一個(gè)可控制的吞吐量。
Parallel Scavenge提供兩個(gè)參數(shù)用于精準(zhǔn)控制吞吐量
Serial old收集器
Serial old收集器是Serial收集器的老年代版本,同樣是一個(gè)單線程收集器,使用標(biāo)記整理算法,它的主要意義也是被Client模式下的虛擬機(jī)使用
Parallel old收集器
Parallel old是Parallel Scavenge收集器的老年代版本,使用多線程和標(biāo)記整理算法。

CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器,它很適合互聯(lián)網(wǎng)站或者B/S系統(tǒng)的服務(wù)端上。
從Mark Sweep名字可知,CMS用的是標(biāo)記清除算法,它的動(dòng)作過(guò)程較之前的復(fù)雜一些,整個(gè)過(guò)程分為4個(gè)步驟:
- 初始標(biāo)記
- 并發(fā)標(biāo)記
- 重新標(biāo)記
- 并發(fā)清除
其中初始標(biāo)記、重新標(biāo)記這兩個(gè)步驟仍然需要 “Stop the world”。初始標(biāo)記只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,速度很快,并發(fā)階段就是進(jìn)行GC Root Tracing的過(guò)程,而重新標(biāo)記則是為了修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運(yùn)行而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,重新標(biāo)記時(shí)間略比初始標(biāo)記長(zhǎng),但遠(yuǎn)比并發(fā)標(biāo)記時(shí)間短。
整個(gè)過(guò)程最耗時(shí)的是并發(fā)標(biāo)記和并發(fā)清除,但用戶線程和收集器線程一起工作,所以總體上說(shuō),CMS收集器的內(nèi)存回收過(guò)程是與用戶線程一起并發(fā)地執(zhí)行。
