JVM03--JVM垃圾收集機制的一些基本概念
前言
今天來學(xué)習(xí)下與JVM垃圾收集機制相關(guān)的一些基本概念。
如何判斷對象是否存活
垃圾收集器首要的任務(wù)的任務(wù)就是判斷哪些對象是存活的,哪些對象已經(jīng)死去了(這里死去的意思是對象不再被任何途徑使用)。
引用計數(shù)算法
引用計數(shù)算法是在對象中添加一個引用計數(shù)器,每當(dāng)有一個地方引用它時,計數(shù)器值就加一;當(dāng)引用失效時,計數(shù)器值就減一;任何時刻計數(shù)器為零的對象就是不可能被使用的對象。
引用計數(shù)算法的缺點就是很難解決對象之間相互循環(huán)引用的問題。
可達(dá)性分析算法
通過一系列稱為“GC
Roots”的根對象作為起始節(jié)點集,從這些節(jié)點開始,根據(jù)引用關(guān)系向下搜索,搜索的過程所走過的路徑稱為“引用鏈”,如果某個對象到GC
Roots間沒有任何引用鏈相連,或者用圖論的話來說就是從GC
Roots到這個對象不可達(dá)時,則證明此對象不可能再被使用的。例如:通過:Objcet 5,Object 6已經(jīng)Object
7三個對象,雖然互有關(guān)聯(lián),但是他們到GC Roots是不可達(dá)的,因此可以判定為是可以回收的對象。
固定的可作為GC Roots對象包括如下幾種:
在虛擬機棧(棧幀中的本地變量表)中引用的對象,譬如各個線程調(diào)用的方法堆棧中使用到的參數(shù),局部變量、臨時變量等
在方法區(qū)中類靜態(tài)屬性引用的對象,譬如Java類的引用類型靜態(tài)變量
在方法區(qū)中常量引用的對象,譬如字符串常量池里的引用
4.在本地方法棧中JNI(即通常所說的Native方法)引用的對象
Java虛擬機內(nèi)部的引用,如基本數(shù)據(jù)類型對應(yīng)的Class對象,一些常駐的異常對象(比如 NullPointException、OutOfMemoryError)等,還有系統(tǒng)類加載器。
所有被同步鎖(synchronized關(guān)鍵字)持有的對象
7.反映Java虛擬機內(nèi)部情況的JMXBean、JVMTI中注冊的回調(diào)、本地代碼緩存等
引用的分類
引用分為如下四種:
強引用: 強引用是指在程序代碼之中普遍存在的引用賦值,即類似“Object obj=new Object()”這種引用關(guān)系。無論任何情況下,只要強引用關(guān)系還存在,垃圾收集器就永遠(yuǎn)不會回收掉被引用的對象
軟引用: 軟引用是用來描述一些還有用,但非必須的對象。只被軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常前,會被這些對象列進回收范圍之中進行第二次回收,如果這次回收還沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常。
弱引用: 弱引用也是用來描述那些非必須對象,但是他的強度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生為止。當(dāng)垃圾收集器開始工作,無論當(dāng)前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象。
虛引用: 一個對象是否有虛引用的存在,完全不會對其生存時間構(gòu)成影響,也無法通過虛引用來取得一個對象實例。為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的只是為了能在這個對象被收集器回收時收到一個系統(tǒng)通知。
回收方法區(qū)
方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:廢棄的常量和不再使用的類型?;厥諒U棄常量
與回收J(rèn)ava堆中的對象非常類似。同時判斷一個常量是否廢棄還是相對簡單,而要判斷一個類型是否屬于“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件。
該類所有的實例都已經(jīng)被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
加載該類的類加載器已經(jīng)被回收,這個通常很難達(dá)成
該類對應(yīng)的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。
分代收集理論
分代收集理論,實質(zhì)是一套符合大多數(shù)程序運行實際情況的經(jīng)驗法則,它建立在兩個分代假說之上。
弱分代假說:絕大多數(shù)對象都是朝生夕滅的。
強分代假說:熬過越多次垃圾收集過程的對象就越難以消亡
收集器應(yīng)該將Java堆劃分出不同的區(qū)域,然后將回收對象依據(jù)其年齡(年齡即對象熬過垃圾收集過程的次數(shù))分配到不同的區(qū)域之中存儲。顯而易見,如果一個區(qū)域中大多數(shù)對象都是朝生夕滅,難以熬過垃圾收集過程的話,那么把他們集中放在一起,每次回收時只關(guān)注如何保留少量存活而不是去標(biāo)記那些大量將要被回收的對象。就能以較低代價回收到大量的空間;如果剩下的都是難以消亡的對象,那把它們集中放在一塊,虛擬機便可以使用較低的頻率來回收這個區(qū)域。
新生代:每次垃圾收集時都發(fā)現(xiàn)有大批對象死去,而每次回收后存活的少量對象,
將會逐步晉升到老年代中存放。
老年代:老年代里面的對象幾乎個個都是在 Survivor 區(qū)域中熬過來的,它們是不會那么容易就 “死掉” 了的。
跨代引用假說:跨代引用相對于同代引用來說僅占極少數(shù)
只需要在新生代上建立一個全局的數(shù)據(jù)結(jié)構(gòu)(該結(jié)構(gòu)被稱為“記憶集”)這個結(jié)構(gòu)把老年代劃分為若干小塊,標(biāo)識出老年代的哪一塊內(nèi)存會存在跨代引用。此后當(dāng)發(fā)生Minor GC時,只有包含了跨代引用的小塊內(nèi)存里的對象才會被加入到GC Roots 進行掃描。
收集說明
部分收集(Partial GC):
指目標(biāo)不是完整收集整個Java堆的垃圾收集。其中又分為:
新生代收集(Minor GC/Young GC):指目標(biāo)只是新生代的垃圾收集。
Minor GC觸發(fā)條件是:
-
Eden區(qū)域滿了,或者新創(chuàng)建的對象大小>Eden所??臻g
-
CMS設(shè)置了CMSScavengeBeforeRemark參數(shù),這樣在CMS的Remark之前會先做一次Minor GC來清理新生代,加速之后的Remark的速度,這樣整體的stop-the-world的時間反而短
-
Full GC的時候也會先觸發(fā)Minor GC。執(zhí)行Minor GC需要注意:
A:當(dāng)JVM無法為一個新的對象分配空間時會觸發(fā)Minor GC,比如當(dāng)Eden區(qū)滿了,所以分配頻率越高,越頻繁執(zhí)行Minor GC。老年代收集(Major GC/Old GC): 指目標(biāo)只是老年代的垃圾收集。目前只有CMS收集器會有單獨收集老年代的行為。
混合收集(Mixed GC):指目標(biāo)是收集整個新生代以及部分老年代的垃圾收集。目前只有G1收集器會有這種行為。
整堆收集(Full GC):
收集整個Java堆和方法區(qū)的垃圾收集,包括新生代、老年代和永久代。Full GC因為需要對整個堆進行回收,所以比Scavenge GC要慢,因此應(yīng)該盡可能減少Full GC的次數(shù)。在對JVM調(diào)優(yōu)的過程中,很大一部分工作就是對Full GC的調(diào)節(jié),有如下原因可能導(dǎo)致Full GC:
當(dāng)老年代無法分配內(nèi)存的時候,會導(dǎo)致Minor GC, 當(dāng)發(fā)生Minor GC的時候可能會觸發(fā)Full GC,由于老年代要對新生代進行擔(dān)保,由于進行一次垃圾回收之前無法確定有多少對象存活,因此老年代并不清楚自己要擔(dān)保多少空間,因此采取用動態(tài)估算的方法:也就是上一次回收發(fā)送是晉升到老年代的對象容量的平均值作為經(jīng)驗值,這樣就會有一個問題,當(dāng)發(fā)生一次Minor GC以后,存活的對象劇增(假設(shè)小對象),此時老年代并沒有滿,但是此時平均值增加了,會造成發(fā)生Full GC。
System.gc()被顯示調(diào)用。
面試題
-
Java中什么樣的對象才能作為GC Roots,GC Roots有哪些呢?
GC管理的主要區(qū)域是Java堆,一般情況下只針對堆進行垃圾回收,方法區(qū)、棧和本地方法區(qū)不被GC所管理,因而選擇這些區(qū)域內(nèi)的對象作為GC roots,被GC roots引用的對象不被GC回收。
GC Root
常說的GC roots,特指的是垃圾收集器(Garbage Collector)的對象,GC會收集那些不是GC Roots且沒有被GC roots引用的對象。
一個對象可以屬于多個Root,GC Root有如下幾種:Class - 由系統(tǒng)類加載器(system class loader)加載的對象,這些類是不能被回收的。他們可以以靜態(tài)字段的方式保存持有其它對象,我們需要注意的一點就是,通過用戶自定義的類加載器的類,除非相應(yīng)的java.lang.Class實例以其它的某種(或多種)方式成為roots,否則它們并不是roots。
Thread — 活著的線程
Stack Local - Java方法的local變量或參數(shù)
JNI Local - JNI方法的local變量或參數(shù)
JNI Global - 全局JNI引用
Monitor Used - 用于同步的監(jiān)控對象。
內(nèi)存池/線程對象和線程快照對象
String常量池
2. 什么時候會觸發(fā)Full GC
當(dāng)對象經(jīng)歷15次Minor GC后,在第16次Minor GC后,如果對象還存活則會被復(fù)制到老年代,在復(fù)制之前,虛擬機會先檢查老年代最大可用的連續(xù)空間是否大于新生代,執(zhí)行這個判斷的原因是,JVM無法知道哪些對象是存活的,所以又相對粗淺的判斷。
如果老年代有足夠的連續(xù)的內(nèi)存空間,則直接Minor GC,如果沒有足夠的空間,那么有兩種選擇,第一種,直接Minor GC,搏一搏,說不定空間還夠,第二種,先對老年代進行Full GC,騰出更多的空間用來Minor GC,至于執(zhí)行哪一種操作,關(guān)鍵看HandlePromotionFailure設(shè)置值是否允許擔(dān)保失敗。
如果不允許擔(dān)保失敗的話,就會Full GC之后再進行復(fù)制,如果允許擔(dān)保失敗。還不會直接Minor GC,還會在進行一次評估,會繼續(xù)檢測老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小。如果大于的話,說明直接Minor GC不出錯的可能性大一些。
通常情況下,還是會設(shè)置HandlePromotionFailure=true,Full GC能少一次就少一次。
以下是邏輯總結(jié):
發(fā)生Minor GC之前,虛擬機會檢測:老年代最大可用的連續(xù)空間>新生代all 對象總空間?
滿足,Minor GC是安全的,可以進行Minor GC
不滿足,虛擬機查看HandlePromotionFailure參數(shù):
(1)為true,允許擔(dān)保失敗,會繼續(xù)檢測老年代最大可用的連續(xù)空間>歷次晉升到老年代對象的平均大小,若大于,將嘗試進行一次Minor GC,若失敗,則重新進行一次Full GC。
(2)為false,則不允許冒險,要進行Full GC(對老年代進行GC)。
作者:碼農(nóng)飛哥
微信公眾號:碼農(nóng)飛哥
