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