注意 : 本系列文章為學(xué)習(xí)系列,部分內(nèi)容會(huì)取自相關(guān)書籍或者網(wǎng)絡(luò)資源,在文章中間和末尾處會(huì)有標(biāo)注
垃圾回收的意義
它使得java程序員不再時(shí)時(shí)刻刻的關(guān)注內(nèi)存管理方面的工作.
垃圾回收機(jī)制會(huì)自動(dòng)的管理jvm內(nèi)存空間,將那些已經(jīng)不會(huì)被使用到了的"垃圾對(duì)象"清理掉",釋放出更多的空間給其他對(duì)象使用.
何為對(duì)象的引用?
Java中的垃圾回收一般是在Java堆中進(jìn)行授艰,因?yàn)槎阎袔缀醮娣帕薐ava中所有的對(duì)象實(shí)例
在java中,對(duì)引用的概念簡(jiǎn)述如下(引用強(qiáng)度依次減弱) :
強(qiáng)引用: 這類引用是Java程序中最普遍的,只要強(qiáng)引用還存在米丘,垃圾收集器就永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象
軟引用: 用來描述一些非必須的對(duì)象,在系統(tǒng)內(nèi)存不夠使用時(shí),這類對(duì)象會(huì)被垃圾收集器回收,JDK提供了SoftReference類來實(shí)現(xiàn)軟引用
弱引用: 用來描述一些非必須的對(duì)象,只要發(fā)生GC,無論但是內(nèi)存是否夠用,這類對(duì)象就會(huì)被垃圾收集器回收,JDK提供了WeakReference類來實(shí)現(xiàn)弱引用
虛引用: 與其他幾種引用不同,它不影響對(duì)象的生命周期,如果這個(gè)對(duì)象是虛運(yùn)用,則就跟沒有引用一樣,在任何時(shí)刻都可能會(huì)回收,JDK提供了PhantomReference類來實(shí)現(xiàn)虛引用
如下為相關(guān)示例代碼
public class ReferenceDemo{
? publicstaticvoidmain(String[] args){
? ? ? //強(qiáng)引用
? ? ? Objectobject=newObject();
? ? ? Object[] objects =newObject[100];
? ? ?//軟引用
? ? ? SoftReference stringSoftReference =newSoftReference<>(newString("SoftReference"));
? ? ? System.out.println(stringSoftReference.get());
? ? ? System.gc();
? ? ? System.out.println(stringSoftReference.get());//手動(dòng)GC,這時(shí)內(nèi)存充足,對(duì)象沒有被回收
? ? //弱引用
? ? ?WeakReference stringWeakReference =newWeakReference<>(newString("WeakReference"));
? ? ?System.out.println(stringWeakReference.get());?
? ? ?System.gc();
? ? ?System.out.println(stringWeakReference.get());//手動(dòng)gc,這時(shí),返回null,對(duì)象已經(jīng)被回收System.out.println();
? ? //虛引用
//虛引用主要用來跟蹤對(duì)象被垃圾回收器回收的活動(dòng)滚粟。//虛引用與軟引用和弱引用的一個(gè)區(qū)別在于:虛引用必須和引用隊(duì)列 (ReferenceQueue)聯(lián)合使用秽之。//當(dāng)垃圾回收器準(zhǔn)備回收一個(gè)對(duì)象時(shí)婶恼,如果發(fā)現(xiàn)它還有虛引用衰齐,就會(huì)在回收對(duì)象的內(nèi)存之前褒翰,把這個(gè)虛引用加入到與之 關(guān)聯(lián)的引用隊(duì)列中
? ? ? ReferenceQueue stringReferenceQueue =newReferenceQueue<>();
? ? ? ?PhantomReference stringPhantomReference =newPhantomReference<>(newString("PhantomReference"), stringReferenceQueue);
? ? ? ?System.out.println(stringPhantomReference.get());? ?
? }
}
當(dāng)然,關(guān)于這幾種引用還有很多知識(shí)點(diǎn),本文只做簡(jiǎn)單的介紹,后續(xù)有機(jī)會(huì)再單獨(dú)的文章詳細(xì)介紹.
如何確定需要回收的垃圾對(duì)象?
引用計(jì)數(shù)器
每個(gè)對(duì)象都有一個(gè)引用計(jì)數(shù)器 , 新增一個(gè)引用的時(shí)候就+1,引用釋放的時(shí)候就-1,當(dāng)計(jì)數(shù)器為0的時(shí)候,就表示可以回收
引用計(jì)數(shù)算法的實(shí)現(xiàn)簡(jiǎn)單盈包,判定效率也很高,在大部分情況下它都是一個(gè)不錯(cuò)的選擇旬牲,當(dāng)Java語言并沒有選擇這種算法來進(jìn)行垃圾回收从祝,主要原因是它很難解決對(duì)象之間的相互循環(huán)引用問題
publicclassLoopReferenceDemo{publicstaticvoidmain(String[] args){? ? ? ? TestA a =newTestA();//1TestB b =newTestB();//2a.b = b;//3b.a = a;//4a =null;//5b =null;//6}? ? }classTestA{publicTestB b;}classTestB{publicTestA a;}
雖然a和b都為null,但是a和b存在循環(huán)引用,這樣a和b就永遠(yuǎn)不會(huì)被回收
如果你在互聯(lián)網(wǎng)上搜索"引用計(jì)數(shù)器"這個(gè)關(guān)鍵字,通常都會(huì)得到以上這一個(gè)結(jié)論,但是究竟為什么a和b不會(huì)被回,收其實(shí)還是沒有說清楚的,下面簡(jiǎn)單說明一下 :
第一行: TestA的引用計(jì)數(shù)器加1,TestA的引用數(shù)量為1
第二行: TestB的引用計(jì)數(shù)器加1,TestB的引用數(shù)量為1
第三行: TestB的引用計(jì)數(shù)器加1,TestB的引用數(shù)量為2
第四行: TestA的引用計(jì)數(shù)器加1,TestA的引用數(shù)量為2
內(nèi)存分布如下圖
第五行: 將a變量設(shè)置為null,不再指向堆中的引用,所以TestA的引用計(jì)數(shù)器減1,TestA的引用數(shù)量為1
第六行: 將b變量設(shè)置為null,不再指向堆中的引用,所以TestB的引用計(jì)數(shù)器減1,TestB的引用數(shù)量為1
內(nèi)存分布如下圖
結(jié)論: 雖然上面程序?qū)和b設(shè)置為null了,但是在堆中,TestA和TestB還是互相持有對(duì)方的引用,引用計(jì)數(shù)器依然不等于0,這個(gè)就稱為循環(huán)引用,所以說"引用計(jì)數(shù)器"會(huì)存在這個(gè)問題,導(dǎo)致這類對(duì)象無法被清理掉.
以上的知識(shí)點(diǎn)參考 :https://www.zhihu.com/question/21539353
可達(dá)性分析
雖然以上的"引用計(jì)數(shù)器"算法存在"循環(huán)引用"的問題,不過目前主流的虛擬機(jī)都采用"可達(dá)性分析(GC Roots Tracing)"算法來標(biāo)記那些對(duì)象是可以被回收的.
該算法是從GC Roots開始向下搜索,搜索走過的路徑稱之為引用鏈.當(dāng)一個(gè)對(duì)象到GC Roots沒有任何引用鏈相連時(shí),就代表這個(gè)對(duì)象是不可用的.稱為"不可達(dá)對(duì)象"
GC Roots包括:
虛擬機(jī)棧(棧幀中的本地變量表)中的引用對(duì)象
方法區(qū)中的靜態(tài)屬性實(shí)體引用的對(duì)象
方法區(qū)中常量引用的對(duì)象
本地方法棧中JNI(Native方法)引用的對(duì)象
實(shí)際上,在根搜索算法中引谜,要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷兩次標(biāo)記過程 :
如果對(duì)象在進(jìn)行根搜索后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈擎浴,那它會(huì)被第一次標(biāo)記并且進(jìn)行一次篩選员咽,篩選的條件是此對(duì)象是否有必要執(zhí)行finalize()方法
當(dāng)對(duì)象沒有覆蓋finalize()方法,或finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過贮预,虛擬機(jī)將這兩種情況都視為沒有必要執(zhí)行
如果該對(duì)象被判定為有必要執(zhí)行finalize()方法贝室,那么這個(gè)對(duì)象將會(huì)被放置在一個(gè)名為F-Queue隊(duì)列中,并在稍后由一條由虛擬機(jī)自動(dòng)建立的仿吞、低優(yōu)先級(jí)的Finalizer線程去執(zhí)行finalize()方法
finalize()方法是對(duì)象逃脫死亡命運(yùn)的最后一次機(jī)會(huì)(因?yàn)橐粋€(gè)對(duì)象的finalize()方法最多只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次), 稍后GC將對(duì)F-Queue中的對(duì)象進(jìn)行第二次小規(guī)模的標(biāo)記滑频,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中讓該對(duì)象重引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)即可
而如果對(duì)象這時(shí)還沒有關(guān)聯(lián)到任何鏈上的引用唤冈,那它就會(huì)被回收掉
如下圖所示
從上圖上看,reference1,2,3都是gc roots
reference1指向instance1,reference2指向instance4,并且instance4又指向了instance6,reference3則指向了instance2
所以說instance1,2,4,6都具有g(shù)c roots可達(dá)性,是存活著的對(duì)象,不會(huì)被垃圾回收器回收掉
而instance3,5則不具備gc roots可達(dá)性,是不可用對(duì)象,將會(huì)被垃圾回收器回收掉
從上圖描述"引用計(jì)數(shù)器"的圖例場(chǎng)景來看,TestA和TestB雖然互相有持有引用,但是并不具備gc roots可達(dá)性,所以,在"可達(dá)性分析"算法下,是會(huì)被垃圾回收器回收掉的
垃圾收集的算法
標(biāo)記-清除 算法
算法分為"標(biāo)記"和"清除"兩個(gè)階段,首先標(biāo)記出需要回收的對(duì)象,在標(biāo)記完成后,統(tǒng)一回收掉之前被標(biāo)記的所有對(duì)象. 它是最基礎(chǔ)的收集算法 . 后續(xù)的收集算法都是基于這種思想,并且對(duì)其缺點(diǎn)進(jìn)行改進(jìn)而產(chǎn)生的
主要缺點(diǎn):
效率問題 : 需要標(biāo)記和清除兩次掃描
空間問題 : 標(biāo)記和清除之后會(huì)產(chǎn)生大量的不連續(xù)的內(nèi)存碎片,可能會(huì)導(dǎo)致,當(dāng)程序需要分配一個(gè)較大內(nèi)存空間的時(shí)候,無法找到足夠的連續(xù)內(nèi)存,從而不得不提前出發(fā)另外一次垃圾回收動(dòng)作
復(fù)制 算法
將可用內(nèi)存按容量劃分為兩塊,每次只使用其中的一塊,當(dāng)內(nèi)存使用完了后,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后在把前面一塊內(nèi)存一次性清理掉
優(yōu)點(diǎn) :
每次只操作一塊內(nèi)存,分配內(nèi)存的時(shí)候無需考慮內(nèi)存碎片的情況,只需要移動(dòng)對(duì)象的指針,按順序分配內(nèi)存即可,實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效
缺點(diǎn) :
會(huì)將內(nèi)存縮小為原來的一半
持續(xù)復(fù)制長(zhǎng)生期的對(duì)象則導(dǎo)致效率降低 (沒理解) (對(duì)于存活率較高的對(duì)象,就會(huì)對(duì)其進(jìn)行多次復(fù)制,從而導(dǎo)致效率降低)
標(biāo)記-壓縮 算法
和標(biāo)記-清除算法一樣,只不過標(biāo)記后的動(dòng)作不是清除,而是將所有對(duì)象向一端移動(dòng),然后直接清理掉邊界以外的對(duì)象(被標(biāo)記的對(duì)象)
特點(diǎn) :
復(fù)制算法比較適合于新生代峡迷,在老年代中,對(duì)象存活率比較高你虹,如果執(zhí)行較多的復(fù)制操作绘搞,效率將會(huì)變低,所以老年代一般會(huì)選用其他算法竟坛,如標(biāo)記—清理算法
該算法標(biāo)記的過程與標(biāo)記—清除算法中的標(biāo)記過程一樣永丝,但對(duì)標(biāo)記后出的垃圾對(duì)象的處理情況有所不同十嘿,它不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有的對(duì)象都向一端移動(dòng)蒿褂,然后直接清理掉端邊界以外的內(nèi)存
分代收集 算法
把java的堆分為"新生代"和"老年代",對(duì)于不同的年代采用不同算法
在新生代中,由于對(duì)象生命周期非常短暫,所以每次垃圾回收的時(shí)候都會(huì)有大量的對(duì)象死去,只有少量存活,這樣,采用"復(fù)制算法",就只需要付出少量存活對(duì)象的復(fù)制成本,就能完成回收
在老年代中,由于對(duì)象生命周期比較長(zhǎng),存活率較高,沒有額外的空間對(duì)它進(jìn)行分配和擔(dān)保,那就必須使用"標(biāo)記-清除算法"或者"標(biāo)記-壓縮算法"來進(jìn)行回收
Minor GC:從年輕代空間(包括Eden和Survivor區(qū)域)回收內(nèi)存被稱為Minor GC
Major GC:清理老年代
Full GC:清理整個(gè)堆空間—包括年輕代和老年代
年輕代:是所有新對(duì)象產(chǎn)生的地方.年輕代被分為3個(gè)部分(Enden區(qū)和兩個(gè)Survivor區(qū),也叫From和To),當(dāng)Eden區(qū)被對(duì)象填滿時(shí),就會(huì)執(zhí)行Minor GC,并把所有存活下來的對(duì)象轉(zhuǎn)移到其中一個(gè)survivor區(qū)(Form),Minor GC同樣會(huì)檢查存活下來的對(duì)象,并把它們轉(zhuǎn)移到另一個(gè)survivor區(qū)(To),這樣在一段時(shí)間內(nèi),總會(huì)有一個(gè)空的survivor區(qū),經(jīng)過多次GC周期后圆米,仍然存活下來的對(duì)象會(huì)被轉(zhuǎn)移到年老代內(nèi)存空間,常這是在年輕代有資格提升到年老代前通過設(shè)定年齡閾值來完成的,需要注意,Survivor的兩個(gè)區(qū)是對(duì)稱的,沒先后關(guān)系,from和to是相對(duì)的.
老年代:在年輕代中經(jīng)歷了N次回收后仍然沒有被清除的對(duì)象,就會(huì)被放到年老代中,都是生命周期較長(zhǎng)的對(duì)象.對(duì)于年老代,則會(huì)執(zhí)行Major GC,來清理.在某些情況下,則會(huì)觸發(fā)Full GC,來清理整個(gè)堆內(nèi)存
元空間:堆外的一部分內(nèi)存,通常直接使用的是系統(tǒng)內(nèi)存,用于存放運(yùn)行時(shí)常量池,等內(nèi)容,垃圾回收對(duì)應(yīng)元空間來說沒有明顯的影響
垃圾收集器
垃圾收集器是內(nèi)存回收算法的具體實(shí)現(xiàn)啄栓,Java虛擬機(jī)規(guī)范中對(duì)垃圾收集器應(yīng)該如何實(shí)現(xiàn)并沒有任何規(guī)定娄帖,因此不同廠商、不同版本的虛擬機(jī)所提供的垃圾收集器都可能會(huì)有很大的差別
Sun HotSpot虛擬機(jī)1.6版包含了如下收集器:Serial谴供、ParNew块茁、Parallel Scavenge、CMS桂肌、Serial Old数焊、Parallel Old
這些收集器以不同的組合形式配合工作來完成不同分代區(qū)的垃圾收集工作,如下是垃圾收集器簡(jiǎn)單介紹 :
Serial收集器
串行收集器,最古老,最穩(wěn)定,以及效率高的收集器,但是可能會(huì)造成程序較長(zhǎng)時(shí)間的停頓,只使用一個(gè)線程去回收.新生代,老年代使用串行回收
新生代使用"復(fù)制算法"
老年代使用"標(biāo)記壓縮算法"
垃圾回收的過程中會(huì)"程序暫停"(Stop the world)
ParNew收集器
是Serial收集器的多線程版,新生代并行,老年代串行
新生代使用"復(fù)制算法"
老年代使用"標(biāo)記壓縮算法"
垃圾回收的過程中會(huì)"程序暫停"(Stop the world)
Paralle收集器
類似于ParNew收集器,但是更關(guān)注系統(tǒng)的吞吐量.
可以通過參數(shù)來打開"自適應(yīng)調(diào)節(jié)策略",虛擬機(jī)會(huì)根據(jù)系統(tǒng)當(dāng)前的運(yùn)行情況收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)整這些參數(shù)以便提供最合適的停頓時(shí)間和最大的吞吐量
也可以通過參數(shù)控制GC的時(shí)間不大于多少毫秒或者比例
新生代使用"復(fù)制算法"
老年代使用"標(biāo)記壓縮算法"
Parallel Old收集器
是Paralle收集器的老年代版本 , 使用多線程和"標(biāo)記-整理算法",這個(gè)收集器在JDK1.6中才開始使用
CMS收集器
是基于"標(biāo)記-清除"算法實(shí)現(xiàn)的,它的運(yùn)作過程相對(duì)于前面的其中收集器要復(fù)雜一些,整個(gè)過程分為4個(gè)步驟,包括 :
初始標(biāo)記(CMS initial mark)
并發(fā)標(biāo)記(CMS concurrent mark)
重新標(biāo)記(CMS remark)
并發(fā)清除(CMS concurrent sweep)
初始標(biāo)記和并發(fā)標(biāo)記仍需要Stop the World.
初始標(biāo)記僅僅只是標(biāo)記一下GC Root能直接關(guān)聯(lián)到的對(duì)象,速度很快.
并發(fā)標(biāo)記階段就是進(jìn)行GC Root Tracing的過程.
重新標(biāo)記這是為了修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記變動(dòng)的那一部分的標(biāo)記記錄,這一階段的停頓時(shí)間會(huì)比初始標(biāo)記階段的時(shí)間稍長(zhǎng)一些,但遠(yuǎn)比并發(fā)標(biāo)記時(shí)間短
整個(gè)過程中耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除過程中,收集器線程可以與用戶線程一起工作,所以總體來說,CMS收集器的內(nèi)存回收是與用戶線程一起并發(fā)執(zhí)行的
優(yōu)點(diǎn) : 并發(fā)收集,低停頓
缺點(diǎn) : 產(chǎn)生大量的空間碎片,并發(fā)階段會(huì)降低吞吐量
G1收集器
與CMS收集器項(xiàng)目,G1收集器有以下特點(diǎn) :
空間整合 :
G1收集器采用標(biāo)記-整理算法,不會(huì)產(chǎn)生空間碎片.分配大對(duì)象時(shí)不會(huì)應(yīng)為找不到連續(xù)的空間而提前觸發(fā)下一次GC
可預(yù)測(cè)停頓 :
降低停頓時(shí)間是G1和CMS的共同關(guān)注點(diǎn)
G1除了追求低停頓外,還能建立可預(yù)測(cè)的停頓時(shí)間模型.
能讓使用者明確指定在一個(gè)長(zhǎng)度為N毫秒的時(shí)間片段內(nèi),消耗在垃圾收集上的時(shí)間不得超過N毫秒,幾乎已經(jīng)是實(shí)時(shí)java(RTSJ)垃圾回收的特征了
上面提到的垃圾收集器,收集的范圍都是整個(gè)新生代或者老年代,而G1不在是這樣.
使用G1收集器的時(shí)候,JAVA堆的內(nèi)存布局與其他的收集器有很大的差別,它將這個(gè)java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region),雖然還保留新生代和老年代的概念,但新生代和老年代不再是物理隔閡了,他們都是一部分(可以不連續(xù))Region的集合
G1的新生代收集器跟ParNew類似,當(dāng)新生代占用達(dá)到一定的比例的時(shí)候,開始觸發(fā)收集
和CMS類似,G1收集器收集老年代對(duì)象的時(shí)候會(huì)有短暫停頓
收集步驟如下 :
標(biāo)記階段 :
首先初始標(biāo)記(initial mark),這個(gè)階段是停頓的(Stop the world event),并且會(huì)觸發(fā)一次普通的Mintor GC(從年輕代空間回收)
Root Region Scanning :
運(yùn)行程序過程中會(huì)回收survivor區(qū)(存活到老年代),這一過程必須在young GC之前完成
Concurrent Marking :
在整個(gè)java堆中進(jìn)行并發(fā)標(biāo)記(和應(yīng)用程序并發(fā)執(zhí)行),此過程可能會(huì)被young GC中斷
若發(fā)現(xiàn)區(qū)域?qū)ο笾械乃袑?duì)象都是垃圾,那個(gè)區(qū)域就會(huì)被立即回收
同時(shí),并發(fā)標(biāo)記過程中,會(huì)去計(jì)算每個(gè)區(qū)域的對(duì)象活性(區(qū)域中存活對(duì)象的比例)
Remark :
再標(biāo)記,會(huì)有短暫停頓(STW)
是用來收集并發(fā)標(biāo)記階段,產(chǎn)生新的垃圾(并發(fā)階段和應(yīng)用程序一同執(zhí)行)
G1中采用了比CMS更快的初始快照算法 : snapshot-at-the-beginning (SATB)
Copy / Clean up :
多線程清除失活對(duì)象,會(huì)有STW
G1將回收區(qū)域的存活對(duì)象拷貝到新的區(qū)域,清除Remember Sets,并發(fā)清空回收區(qū)域,并把它返回到空閑的區(qū)域鏈表中
復(fù)制/清除過程后 :
回收區(qū)域的活性對(duì)象已經(jīng)被收集器回收到"最近復(fù)制的年輕代"(recently copied in young generation)和"最近復(fù)制的老年代"(recently copied in old generation)區(qū)域中了
參考文獻(xiàn)
<<深入理解JVM虛擬機(jī)>>
結(jié)束
本文提到的點(diǎn)很多,有對(duì)象引用,如何定義垃圾對(duì)象,gc算法,現(xiàn)有的垃圾收集器,等.
由于篇幅和時(shí)間原因,每個(gè)點(diǎn)都提及的不深入(當(dāng)然,本篇文章的每個(gè)點(diǎn)深入的聊起來,都?jí)驅(qū)懕緯牧?呵呵).
后續(xù)會(huì)找機(jī)會(huì)逐個(gè)的將這些點(diǎn)跟大家深入的討論.
總之 "學(xué)無止境" , 與大家共勉 .