概述
對于java程序員酝豪,在虛擬機自動內(nèi)存管理機制下园骆,不再需要為每一個new操作去寫配對的delete/free代碼,不容易出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出的問題寓调。垃圾回收機制會自動管理jvm內(nèi)存空間,將不會被使用到的垃圾對象清理锄码,釋放空間被其他對象使用夺英。
垃圾收集(Garbage Collection,GC)需要完成3件事情:
- 哪些內(nèi)存需要回收
- 什么時候回收
- 如何回收
引用
判定對象是否存活都與引用有關(guān),在jdk1.2之后滋捶,java對引用的概念進(jìn)行了擴充痛悯,將引用分為強引用(Strong Reference)
、軟引用(Soft Reference)
重窟、弱引用(Weak Reference)
载萌、虛引用(Phantom Reference)
,這4種引用強度依次減弱巡扇。
-
強引用:強引用在代碼種普遍存在扭仁,例如
Object obj = new Object()
這類代碼。只要強引用存在厅翔,垃圾收集器永遠(yuǎn)不會回收被引用的對象乖坠。 -
軟引用:描述一些還有用但非必需的對象。當(dāng)系統(tǒng)內(nèi)存不足時刀闷,在發(fā)生內(nèi)存溢出之前熊泵,這類對象會被垃圾收集器回收。JDK提供
SoftReference
類實現(xiàn)軟引用甸昏。 -
弱引用:用于描述非必需對象顽分,強度比軟引用弱。只要發(fā)生GC施蜜,這類對象都會被垃圾收集器回收卒蘸,JDK提供
WeakReference
類實現(xiàn)弱引用。 -
虛引用:最弱的一種引用關(guān)系花墩。虛引用不會對象的生存時間構(gòu)成影響悬秉,也無法通過虛引用取得對象實例澄步,它唯一的目的是能在這個對象被垃圾收集器回收時收到一條系統(tǒng)通知。JDK提供
PhantomReference
類實現(xiàn)虛引用和泌。
如何判定對象是否存活
在堆種村缸,存放著java幾乎所有的對象實例,垃圾收集器對堆進(jìn)行回收前武氓,第一步就是確定哪些對象還“存活”梯皿,哪些對象已經(jīng)“死去”(即不可能再被任何途徑使用的對象)。
引用計數(shù)算法
每個對象有個引用計數(shù)器县恕,每當(dāng)有一個地方引用它時东羹,計數(shù)器值+1;引用失效時忠烛,計數(shù)器值-1属提;任何時刻計數(shù)器值為0的對象就是不可能再被使用的。
優(yōu)點:
- 實現(xiàn)簡單美尸,判定效率高
缺點:
- 很難解決對象之間相互循環(huán)引用的問題
/**
* @Time : 2019/05/12 上午 10:42
* @Author : xiuc_shi
**/
public class ReferenceCountingGC {
private ReferenceCountingGC instance;
public static void main(String[] args) {
ReferenceCountingGC a = new ReferenceCountingGC();
ReferenceCountingGC b = new ReferenceCountingGC();
a.instance = b;
b.instance = a;
a = null;
b = null;
System.gc();
}
}
以上例子冤议,a對象引用了b對象,b對象引用了a對象师坎,但最后a和b都指向null了恕酸,實際上兩個對象不能再被訪問。如果使用引用計數(shù)算法胯陋,它們將不會被回收蕊温。
可達(dá)性分析算法
通過一系列“GC Roots”對象作為起始點,從這些節(jié)點開始向下搜索遏乔,搜索所走過的路徑稱為引用鏈(Reference Chain)义矛,當(dāng)一個對象到GC Roots沒有任何引用鏈時,則證明此對象不可用盟萨。
對象object5症革、object6和object7雖然相互關(guān)聯(lián),但到GC Roots不可達(dá)鸯旁,因此被判定為可回收對象噪矛。
在java語言種,可作為GC Roots的對象包括:
- 虛擬機棧(棧幀種本地變量表)種引用的對象
- 方法區(qū)種類靜態(tài)屬性引用的對象
- 方法區(qū)種常量引用的對象
- 本地方法棧種JNI(native 方法)引用的對象
即使被可達(dá)性分析算法標(biāo)記為不可達(dá)對象铺罢,對象也并非非死不可艇挨。要真正宣告一個對象死亡,至少要經(jīng)歷兩次標(biāo)記過程:
- 對象被可達(dá)性分析不可到達(dá)GC Roots時韭赘,它將會被第一次標(biāo)記并進(jìn)行一次篩選缩滨,篩選的條件是此對象是否有必要執(zhí)行
finalize()
方法。 - 當(dāng)對象沒有覆蓋
finalize()
方法,或者finalize()
方法已經(jīng)被虛擬機調(diào)用過脉漏,則將這兩種情況視為“沒有必要執(zhí)行”苞冯。 - 如果對象被判定為有必要執(zhí)行
finalize()
方法,則對象會放置在一個叫做F-Queue
的隊列中侧巨,并在稍后由虛擬機自動建立的舅锄、低優(yōu)先級的Finalizer線程執(zhí)行。 -
finalize()
方法是對象逃脫死亡命運的最后一次機會司忱,稍后GC將堆F-Queue
隊列中的對象進(jìn)行第二次小規(guī)模標(biāo)記皇忿,如果對象在finalize()
中重新與引用鏈上的任意一個對象建立關(guān)聯(lián),那么第二次標(biāo)記時它會被移除出“即將回收”的集合坦仍。 - 這種自救的機會只有一次鳍烁,因為一個對象的
finalize()
方法最多只會被系統(tǒng)自動調(diào)用一次。
垃圾收集算法
標(biāo)記-清除算法
標(biāo)記-清除(Mark-Sweep)算法是最基礎(chǔ)得收集算法繁扎,算法分為“標(biāo)記”和“清除”兩個階段:首先標(biāo)記出所有需要回收得對象幔荒,標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記得對象。
該算法主要有兩個不足:
- 效率問題:標(biāo)記和清除兩個過程效率都不高梳玫。
-
空間問題:標(biāo)記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片铺峭,空間碎片太多可能會導(dǎo)致程序分配較大對象時,無法找到足夠的連續(xù)內(nèi)存而出發(fā)另一次收集動作汽纠。
復(fù)制算法
復(fù)制算法是為了解決標(biāo)記-清除算法的效率問題而出現(xiàn)的。它將可用的內(nèi)存容量劃分為大小相等的兩塊傀履,每次只使用其中的一塊虱朵。當(dāng)這塊內(nèi)存用完后,將還存活的對象復(fù)制到另一塊,然后把已使用過的內(nèi)存空間清理。這樣內(nèi)存分配時不用考慮內(nèi)存碎片的問題瘩绒,只需要按順序分配內(nèi)存癌压,實現(xiàn)簡單,運行高效泳梆。但是每次只能使用內(nèi)存空間的一半。
實際上,由于新生代中的對象98%都是“朝生夕死”偿荷,所以一般內(nèi)存不是按照“1:1”的比例來劃分的。而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間唠椭,每次使用Eden和其中一塊Survivor跳纳。當(dāng)回收時,將Eden和Survivor中還存活的對象一次性復(fù)制到另一塊Survivor空間上贪嫂,最后將Eden和剛使用過的Survivor空間寺庄。
HotSpot虛擬機默認(rèn)Eden和Survivor的大小比例是8:1,只有10%的空間被“浪費”。
標(biāo)記-整理算法
復(fù)制算法在對象存活率較高時會進(jìn)行較多的復(fù)制操作斗塘,效率將會降低赢织,所以在老年代一般不能直接用復(fù)制算法。
根據(jù)老年代的特點馍盟,標(biāo)記-整理(Mark-Compact)算法被提出于置。它的標(biāo)記過程和標(biāo)記-清除算法一樣,但是后續(xù)不是直接對可回收對象進(jìn)行清理朽合,而是讓所有存活的對象都向一端移動俱两,然后直接清理端邊界以外的內(nèi)存。
分代收集算法
當(dāng)前商業(yè)虛擬機的垃圾收集都采用分代收集(Generational Collection)算法曹步,該算法沒有什么新思想宪彩,只是根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊。一般把java堆分為新生代和老年代讲婚,根據(jù)各個年代的特點采用適當(dāng)?shù)氖占惴ā?/p>
- 新生代中尿孔,每次垃圾收集時都會發(fā)現(xiàn)大量對象死去,選用復(fù)制算法筹麸,只需要將少量存活對象賦值到另一個空間活合。
- 老年代中,對象存活率高物赶,不適合使用復(fù)制算法白指,而使用“標(biāo)記-清理”或者“標(biāo)記-整理”算法來進(jìn)行回收。
垃圾收集器
如果說收集算法是垃圾回收的方法論酵紫,那么垃圾收集器就是內(nèi)存回收的具體實現(xiàn)告嘲。
Serial收集器
Serial收集器是最基本、發(fā)展歷史最悠久的收集器奖地,它只使用一個線程去回收內(nèi)存橄唬。新生代采用復(fù)制算法,老年代使用標(biāo)記-整理算法参歹,在內(nèi)存回收過程中會停掉其他所有的工作線程(Stop The World)仰楚,直到收集結(jié)束。
Serial收集器是運行在Client模式下的虛擬機最好垃圾回收器選擇犬庇。
ParNew收集器
ParNew收集器是Serial收集器的多線程版本僧界,除了使用多線程,其他的一些控制參數(shù)臭挽、收集算法捎泻、Stop The World、對象分配規(guī)則埋哟、回收策略等與Serial收集器一樣笆豁。
ParNew收集器是許多運行在Server模式下的虛擬機首選的新生代收集器郎汪。其次,它是除Serial收集器外闯狱,唯一能與CMS收集器配合工作的收集器煞赢。
Parallel Scavenge收集器
Parallel Scavenge收集器是一個新生代收集器,使用了復(fù)制算法哄孤。與其他垃圾收集器的關(guān)注點不同照筑,它的目標(biāo)是達(dá)到一個可控制的吞吐量(Throughput)。
吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)
Parallel Scavenge收集器提供了兩個參數(shù)用于精確控制吞吐量瘦陈,分別為控制最大垃圾收集停頓時間(-XX:MaxGCPauseMillis
)和直接設(shè)置吞吐量大小(-XX:GCTimeRatio
)凝危,通過動態(tài)調(diào)整這兩個參數(shù)以得到合適的停頓時間和吞吐量。
Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本晨逝,單線程蛾默,使用標(biāo)記-整理算法。它有兩個用途:
- 和Parallel Scavenge收集器搭配使用
- 作為CMS收集器的備選方案
Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本捉貌,多線程支鸡,使用標(biāo)記-整理算法。如果選擇了新生代Parallel Scavenge收集器趁窃,那么除了Serial Old收集器外別無選擇牧挣。但是這種組合,由于單線程的老年代收集無法充分發(fā)揮服務(wù)器多核的處理能力醒陆。直到Parallel Old的出現(xiàn)瀑构,在注重吞吐量以及CPU資源敏感的場合,會優(yōu)先考慮Parallel Scavenge+Parallel Old收集器組合刨摩。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標(biāo)的收集器寺晌。java應(yīng)用大多集中在網(wǎng)站或B/S系統(tǒng)的服務(wù)端上,尤其重視服務(wù)的響應(yīng)速度码邻,希望系統(tǒng)的停頓時間最短以提高用戶體驗。
CMS使用了標(biāo)記-清除算法另假,整個過程分為4個步驟:
- 初始標(biāo)記(CMS initial mark)
- 并發(fā)標(biāo)記(CMS concurrent mark)
- 重新標(biāo)記(CMS remark)
- 并發(fā)清除(CMS concurrent sweep)
初始標(biāo)記像屋、重新標(biāo)記仍然需要Stop The World。初始標(biāo)記僅僅標(biāo)記了一下GC Roots能直接關(guān)聯(lián)到的對象边篮,速度很快己莺;并發(fā)標(biāo)記則是進(jìn)行GC Roots Tracing的過程,重新標(biāo)記是為了修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運作而導(dǎo)致標(biāo)記產(chǎn)生變動的那部分對象的標(biāo)記記錄戈轿。
CMS收集器的優(yōu)點:
- 并發(fā)收集凌受,低停頓
CMS收集器的缺點:
- CMS收集器對CPU資源非常敏感。
- CMS收集器無法處理浮動垃圾思杯。
- 基于標(biāo)記-清除算法胜蛉,會產(chǎn)生大量空間碎片挠进。
G1收集器
G1(Garbage-First)收集器是當(dāng)今收集器技術(shù)發(fā)展的最前沿成果之一,它是面向服務(wù)器應(yīng)用的垃圾收集器誊册。
與其他GC收集器相比领突,G1具備的特點如下:
- 并行與并發(fā):充分利用多CPU、多核環(huán)境案怯,縮短Stop The World時間君旦,而且可以并發(fā)執(zhí)行內(nèi)存回收。
- 分代收集:可以不與其他收集器配合獨立管理整個GC堆嘲碱,采用不同的方式處理新建對象和已存活一段時間金砍、熬過多個GC的舊對象。
- 空間整合:G1從整體上看基于標(biāo)記-整理算法實現(xiàn)麦锯,局部上看基于復(fù)制算法實現(xiàn)恕稠。因此,不會產(chǎn)生空間碎片离咐。
- 可預(yù)測的停頓:降低停頓時間是G1和CMS的共同關(guān)注點谱俭;建立了可預(yù)測的停頓時間模型,能讓使用者明確在一個長度為M的時間片內(nèi)宵蛀,消耗在垃圾回收上的時間不超過N昆著,幾乎是實時java(RTSJ)垃圾回收的特征了。
G1之前的收集器進(jìn)行回收的范圍是整個新生代或老年代术陶,而使用G1收集器時凑懂,java堆的內(nèi)存布局與其他收集器有很大的差別,它將整個java堆分為多個大小相等的獨立區(qū)域(Region)梧宫,雖然還保留新生代和老年代的概念接谨,但新生代和老年代不再是物理隔離,它們都是一部分Region(不一定連續(xù))的集合塘匣。
G1收集器可以有計劃地避免在整個java堆中進(jìn)行全區(qū)域的垃圾收集脓豪。G1跟蹤各個Region中的垃圾堆積的價值大小(回收所獲得空間大小以及回收所需時間的經(jīng)驗值),在后臺維護(hù)一個優(yōu)先列表忌卤,每次根據(jù)允許的收集時間扫夜,優(yōu)先回收價值最大的Region。這種使用Region劃分內(nèi)存空間以及有優(yōu)先級的區(qū)域回收方式驰徊,保證了G1收集器在有限時間內(nèi)獲得盡可能高的收集效率笤闯。
G1收集器每個Region有一個與之對應(yīng)的Remembered Set,虛擬機發(fā)現(xiàn)程序在對Reference類型數(shù)據(jù)進(jìn)行寫操作時棍厂,會產(chǎn)生Write Barrier暫時中斷寫操作颗味,檢查Reference引用的對象是否處于不同的Region,如果是,則通過CardTable把相關(guān)信息記錄到被引用對象所屬Region的Remembered Set中牺弹。當(dāng)進(jìn)行內(nèi)存回收時浦马,在GC根節(jié)點的枚舉范圍內(nèi)加入Remembered Set即可保證不對全堆掃面时呀。
G1的運作大致劃分為以下步驟:
- 初始標(biāo)記(Initial Marking):標(biāo)記GC Roots能直接關(guān)聯(lián)的對象,并修改TAMS(Next Top at Mark Start)的值捐韩,讓下一階段用戶程序并發(fā)運行時退唠,能在正確可用的Region中創(chuàng)建新對象。
- 并發(fā)標(biāo)記(Concurrent Marking):對堆中對象進(jìn)行可達(dá)性分析荤胁,找到存活的對象瞧预。
- 最終標(biāo)記(Final Marking):修正在并發(fā)標(biāo)記期間因用戶程序繼續(xù)運作而導(dǎo)致標(biāo)記產(chǎn)生變化的那一部分標(biāo)記記錄,虛擬機將這段時間對象變化記錄在Remembered Set Logs中仅政,最終標(biāo)記階段將Remembered Set Logs的數(shù)據(jù)合并到Remembered Set垢油。
- 篩選回收(Live Data Counting and Evacuation):首先對各個Region的回收價值和成本進(jìn)行排序,根據(jù)用戶所期望的GC停頓時間指定回收計劃圆丹。