JVM的垃圾回收機制你了解嗎
一:回收堆內(nèi)存
1.如何判定對象已死(可達性分析算法)
當前主流的商用程序語言的內(nèi)存管理子系統(tǒng),都是通過可達性分析算法來判定對象是否存活的。這個算法的基本思路就是通過一系列稱為“GC Roots”的根對象作為起始節(jié)點集,從這些節(jié)點開始,根據(jù)引用關(guān)系向下搜索,搜索過程所走過的路徑稱為“引用鏈”,如果某個對象到GC Roots間沒有任何引用鏈相連,或者用圖論的話來說就是從GC Roots到這個對象不可達時,則證明此對象是不可能再被使用的。
在Java技術(shù)體系里面,固定可作為GC Roots的對象包括以下幾種:
- 在虛擬機棧中引用的對象,譬如各個線程被調(diào)用的方法堆棧中使用到的參數(shù)、局部變量、臨時變量等;
- 在方法區(qū)中類靜態(tài)屬性引用的對象,譬如Java類的引用類型靜態(tài)變量;
- 在方法區(qū)中常量引用的對象,譬如字符串常量池里的引用;
- 在本地方法棧中引用的對象;
- JVM內(nèi)部的引用,如基本數(shù)據(jù)類型對應的Class對象,常駐的異常對象(如NullPointExcepiton),以及系統(tǒng)類加載器;
- 所有被同步鎖(synchronized關(guān)鍵字)持有的對象;
- 反映Java虛擬機內(nèi)部情況的JMXBean、JVMTI中注冊的回調(diào)、本地代碼緩存等。
2.對象的引用級別
強引用:強引用是最傳統(tǒng)的“引用”的定義,是指在程序代碼之中普遍存在的引用賦值,即類似“Object obj=new Object()”這種引用關(guān)系。無論任何情況下,只要強引用關(guān)系還存在,垃圾收集器就永遠不會回收掉被引用的對象。
軟引用:軟引用是用來描述一些還有用,但非必須的對象。只被軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常前,會把這些對象列進回收范圍之中進行第二次回收,如果這次回收還沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常。
弱引用:弱引用也是用來描述那些非必須的對象,但是它的強度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生為止。當垃圾收集器開始工作,無論當前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象。
虛引用:虛引用是最弱的一種引用關(guān)系。一個對象是否有虛引用的存在,完全不會對其生存時間構(gòu)成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關(guān)聯(lián)的唯一目的只是為了能在這個對象被收集器回收時收到一個系統(tǒng)通知。
3.對象的死亡過程
真正宣告一個對象死亡,至少要經(jīng)歷兩次標記過程:
1.第一次標記:如果對象在進行可達性分析后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈,那它將會被第一次標記,隨后進行一次篩選,篩選的條件是此對象是否有必要執(zhí)行finalize()方法。假如對象沒有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機調(diào)用過,那么虛擬機將這兩種情況都視為“沒有必要執(zhí)行”。反之,該對象將會被放置在一個名為F-Queue的隊列之中,并在稍后由一條由虛擬機自動建立的、低調(diào)度優(yōu)先級的Finalizer線程去執(zhí)行它們的==finalize()==方法。
2.第二次標記:稍后,收集器將對F-Queue中的對象進行第二次小規(guī)模的標記。如果對象要在==finalize()==中成功拯救自己,只要重新與引用鏈上的任何一個對象建立關(guān)聯(lián)即可,譬如把自己(this)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移出“即將回收”的集合。如果對象這時候還沒有逃脫,那基本上它就真的要被回收了。
二:垃圾回收算法
分代收集理論
依據(jù)分代假說理論,垃圾回收可以分為如下幾類:
1.新生代收集(Minor GC/Young GC):目標為新生代的垃圾收集。
2.老年代收集(Major GC/Old GC):目標為老年代的垃圾收集,目前只有CMS收集器會有這種行為。
3.混合收集(Mixed GC):目標為整個新生代及部分老年代的垃圾收集,目前只有G1收集器會有這種行為。
4.整堆收集(Full GC):目標為整個堆和方法區(qū)的垃圾收集。
1.標記清除算法
算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成后,統(tǒng)一回收掉所有被標記的對象,也可以反過來,標記存活的對象,統(tǒng)一回收所有未被標記的對象。它的主要缺點有兩個:
(1)執(zhí)行效率不穩(wěn)定,進行大量標記和清除的動作
(2)內(nèi)存空間碎片化
2.標記復制算法
將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內(nèi)存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。對于多數(shù)對象都是可回收的情況,算法需要復制的就是占少數(shù)的存活對象,而且每次都是針對整個半?yún)^(qū)進行內(nèi)存回收,分配內(nèi)存時也就不用考慮有空間碎片的復雜情況,只要移動堆頂指針,按順序分配即可。
這種復制回收算法的代價是將可用內(nèi)存縮小為了原來的一半,空間浪費未免太多了一點。另外,如果內(nèi)存中多數(shù)對象都是存活的,這種算法將會產(chǎn)生大量的內(nèi)存間復制的開銷。所以,現(xiàn)在的商用Java虛擬機大多都優(yōu)先采用了這種收集算法去回收新生代。
改進:
IBM公司曾有一項專門研究對新生代“朝生夕滅”的特點做了更量化的詮釋——新生代中的對象有98%熬不過第一輪收集。因此并不需要按照1∶1的比例來劃分新生代的內(nèi)存空間。在1989年,Andrew Appel針對具備“朝生夕滅”特點的對象,提出了一種更優(yōu)化的半?yún)^(qū)復制分代策略,現(xiàn)在稱為“Appel式回收”。
Appel式回收的具體做法是把新生代分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次分配內(nèi)存只使用Eden和其中一塊Survivor。發(fā)生垃圾搜集時,將Eden和Survivor中仍然存活的對象一次性復制到另外一塊Survivor空間上,然后直接清理掉Eden和已用過的那塊Survivor空間。
HotSpot虛擬機的Serial、ParNew等新生代收集器均采用了這種策略來設計新生代的內(nèi)存布局。
HotSpot虛擬機默認Eden和Survivor的大小比例是8:1:1,也即每次新生代中可用內(nèi)存空間為整個新生代容量的90%(Eden的80%加上一個Survivor的10%),只有一個Survivor空間,即10%的新生代是會被“浪費”的。
98%的對象可被回收僅僅是“普通場景”下測得的數(shù)據(jù),任何人都沒有辦法百分百保證每次回收都只有不多于10%的對象存活,因此Appel式回收還有一個充當罕見情況的“逃生門”的安全設計,當Survivor空間不足以容納一次Minor GC之后存活的對象時,就需要依賴其他內(nèi)存區(qū)域(大多就是老年代)進行分配擔保。
3.標記整理算法
其中的標記過程仍然與“標記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向內(nèi)存空間一端移動,然后直接清理掉邊界以外的內(nèi)存。
如果移動存活對象,尤其是在老年代這種每次回收都有大量對象存活區(qū)域,移動存活對象并更新所有引用這些對象的地方將會是一種極為負重的操作,而且這種對象移動操作必須全程暫停用戶應用程序才能進行,像這樣的停頓被最初的虛擬機設計者形象地描述為“Stop The World”。
三:垃圾收集器
1.G1(Garbage First)
在G1收集器出現(xiàn)之前的所有其他收集器,垃圾收集的目標范圍要么是整個新生代(Minor GC),要么就是整個老年代(Major GC),再要么就是整個Java堆(Full GC)。而G1跳出了這個限制,它可以面向堆內(nèi)存任何部分來組成回收集進行回收,衡量標準不再是它屬于哪個分代,而是哪塊內(nèi)存中存放的垃圾數(shù)量最多,回收收益最大,這就是G1收集器的Mixed GC模式。
G1將Java堆劃分為多個大小相等的獨立區(qū)域(Region),JVM目標是不超過2048個Region(JVM源碼里TARGET_REGION_NUMBER 定義),實際可以超過該值,但是不推薦。一般Region大小等于堆大小除以2048,比如堆大小為4096M,則Region大小為2M,當然也可以用參數(shù) “-XX:G1HeapRegionSize” 手動指定Region大小,但是推薦默認的計算方式。
G1保留了年輕代和老年代的概念,但不再是物理隔閡了,它們都是(可以不連續(xù))Region的集合。每一個Region都可以根據(jù)需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。還有一類專門用來存儲大對象的特殊區(qū)域(Humongous Region)
更具體的處理思路是,讓G1收集器去跟蹤各個Region里面的垃圾堆積的“價值”大小,價值即回收所獲得的空間大小以及回收所需時間的經(jīng)驗值,然后在后臺維護一個優(yōu)先級列表,每次根據(jù)用戶設定允許的收集停頓時間(默認是200毫秒),優(yōu)先處理回收價值收益最大的那些Region,這也就是“Garbage First”名字的由來。
一個Region可能之前是年輕代,如果Region進行了垃圾回收,之后可能又會變成老年代,也就是說Region的區(qū)域功能可能會動態(tài)變化。
G1垃圾收集器對于對象什么時候會轉(zhuǎn)移到老年代跟之前講過的原則一樣,唯一不同的是對大對象的處理,G1有專門分配大對象的Region叫Humongous區(qū),而不是讓大對象直接進入老年代的Region中。在G1中,大對象的判定規(guī)則就是一個大對象超過了一個Region大小的50%,比如按照上面算的,每個Region是2M,只要一個大對象超過了1M,就會被放入Humongous中,而且一個大對象如果太大,可能會橫跨多個Region來存放。
Humongous區(qū)專門存放短期巨型對象,不用直接進老年代,可以節(jié)約老年代的空間,避免因為老年代空間不夠的GC開銷。
Full GC的時候除了收集年輕代和老年代之外,也會將Humongous區(qū)一并回收。
G1收集器的運作過程大致可劃分為以下四個步驟:
1.初始標記:暫停所有的其他線程,并記錄下gc roots直接能引用的對象,速度很快
2.并發(fā)標記:從GC Root開始對堆中對象進行可達性分析,遞歸掃描整個堆里的對象圖,找出要回收的對象,這階段耗時較長,但可與用戶程序并發(fā)執(zhí)行。
3.最終標記:對用戶線程做另一個短暫的暫停,修正并發(fā)標記期間,因用戶程序繼續(xù)運作而導致標記產(chǎn)生變動的那一部分對象的標記記錄。
4.篩選回收:負責更新Region的統(tǒng)計數(shù)據(jù),對各個Region的回收價值和成本進行排序,根據(jù)用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構(gòu)成回收集,然后把決定回收的那一部分Region的存活對象復制到空的Region中,再清理掉整個舊Region的全部空間。這里的操作涉及存活對象的移動,是必須暫停用戶線程,由多條收集器線程并行完成的。
G1從整體來看是基于標記整理算法實現(xiàn)的收集器,但從局部上看又是基于標記復制算法實現(xiàn)。無論如何,這兩種算法都意味著G1運作期間不會產(chǎn)生內(nèi)存空間碎片,垃圾收集完成之后能提供規(guī)整的可用內(nèi)存。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
Spring Cloud超詳細i講解Feign自定義配置與使用
這篇文章主要介紹了SpringCloud Feign自定義配置與使用,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-06-06關(guān)于實體類中Date屬性格式化@JsonFormat @DateTimeFormat
這篇文章主要介紹了關(guān)于實體類中Date屬性格式化@JsonFormat @DateTimeFormat問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07