1. 對象存活判斷
1.1. 引用計數算法 Reference Counting
- 給對象添加一個引用計數器,每當有一個地方引用它的時候,計數器值就加一;當引用失效時,計數器值就減一;任何時刻計數器為0的對象就是不可能再被使用的。
- 主流的JVM沒有選用引用計數算法來管理內存,主要的原因是它很難解決對象之間的相互循環引用的問題。
1.2. 可達性分析算法 Reachability Analysis
- 通過一系列稱為“GC-Roots”的對象作為起點,從這些結點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的(圖論中的不可達)。
- 可作為GC Roots的對象:
虛擬機棧(戰爭中的本地變量表)中引用的對象
方法區中類靜態屬性引用的對象
方法區中常量引用的對象
本地方法棧中JNI引用的對象
1.3. 引用類型 Reference
- 強引用:Strong Reference
指的是類似于
Object object = new Object()
這類引用,只要強引用存在,垃圾收集器就永遠不會回收被引用對象。
- 軟引用:Soft Reference
描述一些還有用但并非必要的對象。JDK提供了SoftReference來實現軟引用
在系統快要發生內存溢出之前,將會把這些對象列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。
- 弱引用: Weak Reference
用來描述非必須對象,它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。JDK提供了WeakReference類來實現弱引用。
當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。
- 虛引用:Phantom Reference
也稱為幽靈引用或幻影引用,它是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。JDK提供PhantomReference類來實現虛引用
為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。
1.3. 引用類型 Reference
-
不可達對象,會暫時處于“緩刑”階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:
如果對象在進行可達性分析后發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記并且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為“沒有必要執行”。
finalize()方法是對象逃脫死亡命運的最后一次機會,稍后GC將對F-Queue中的對象進行第二次小規模的標記,如果對象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的被回收了。
注:如果對象唄判定有必要執行finalize()方法,那么這個對象將會放置在一個叫做F-Queue隊列中,并隨后JVM會創建一個低優先級的Finalizer線程去執行它。JVM觸發這個方法,并不確保它會執行結束,因為如果對象finalize方法如果執行緩慢或者死循環,將很有可能會導致F-Queue隊列其他對象永久等待,甚至導致整個內存回收系統奔潰。
2. 垃圾收集算
2.1. 標記-清除算法 Mark-Sweep
-
算法分兩個階段,即標記和清除。
- 標記處所需要回收的對象
- 標記完成后統一回收所有被標記對象
-
算法主要不足
- 效率問題,標記和清除兩個過程效率都不高
- 空間問題,標記清除后悔產生大量不連續的空間
空間碎片太多可能會導致以后分配大對象時無法找到足夠連續內存存放而不得不觸發另一次垃圾收集。
2.2. 復制算法 Copying
- 將可用的內存按照容量劃分為大小相等的兩塊,每次使用其中一塊。當前一塊用完了,將還存活的對象移動到另一塊上面,然后把已使用過的內存空間一次性清理掉。這樣每次都是堆整個半區進行內存回收,分配內存時也就不考慮內存碎片等復雜情況,實現簡單、運行高效。代價是將內存縮小為原來的一半。
2.3. 標記-整理算法 Mark-Compact
- 標記后不直接對可回收對象清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以為的內存。
2.4. 分代收集算法 Generational Collection
-
把JVM堆內存分為新生代和老年代,對不同的年代采取不同的收集算法。
在新生代中每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法。
老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,那就必須使用“標記-清理”或“標記-整理”算法來進行回收。
3. 垃圾收集算
3.1. 枚舉根節點
- 可達性分析從GC Roots節點找引用鏈操作,現在引用僅方法區就有數百兆,逐個檢查里面的引用非常耗時。
- 可達性分析對執行時間的敏感上體現在GC停頓上,這項分析工作必須在一個能確保一致性的快照中,
這里的一致性是指在整個分析期間整個執行系統開起來像被凍結在某個時間節點上。如果這點不滿足準確性就無法保證。這是導致GC進行時必須停頓所有Java執行線程的其中一個重要原因。即使在CMS收集器(號稱幾乎不發生停頓)中枚舉根節點也是必須要停頓的。
- 主流的JVM都是使用的準確式GC,所以當執行系統停頓下來并不需要一個不漏檢查完所有執行上下文和全局的引用位置,JVM知道哪里存放這個信息,在HotSpot使用了一組OopMap的數據結構來達到這個目的。
在類加載完后,HotSpot吧對象內的各個偏移量上的類型計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。在GC掃描時,就可以直接知道這些信息。
3.2. 安全點 Safepoint
HotSpot在特定的位置記錄棧和寄存器中哪些位置是引用,這個“特定位置”就稱為“安全點”,即程序執行時并非在所有地方都能停頓下來開始GC,只有在到達安全點時才能暫停。
-
安全點不能太多,也不能太少,太多增大系統負荷,太少GC等待時間太長。所以安全點的選擇基本是以“是否具有讓程序長時間執行的特征”為標準選定。
因為每條指令執行時間都非常短暫,程序不太可能因為指令流長度太長而過長時間運行,所以長時間的特征就是指令序列復用、循環跳轉、異常跳轉等
-
怎樣確保GC發生所有線程都跑到安全點再停頓下來,有兩種方案:
搶先式中斷(Preemptive Suspension):在發生GC時,首先把所有線程全部中斷,如果發現有線程中斷的地方不在安全點上,就恢復線程,讓它跑到安全點上。
主動式中斷(Voluntary Suspension):當GC需要中斷線程時,不對線程直接操作,僅簡單設置一個標志,各個線程執行時主動去輪詢這個標志,發現中斷標志為真的時候就自己把中斷掛起。輪詢標志這個地方和安全點是重合的,另外再加上創建對象需要分配內存的地方。
3.3. 安全區域 Safe Region
- 安全區域是指一段代碼片段中,引用關系不會發發生變化。在這個區域中的任意地方開始GC都是安全的。
- 在線程執行到Safe Region中的代碼時,首先表示自己進入了Safe Region,這這段時間里,JVM要發起GC時,就不用管標識自己為Safe Region狀態的線程了。在線程要離開Safe Region時,它要檢查系統是否已經完成了根節點枚舉(或者整個GC過程),如果完成了,那線程就繼續執行,否則它就必須等待知道收到可以安全離開Safe Region的信號為止。
4. 垃圾收集器
4.1. Serial收集器
- 是一個單線程垃圾收集器,它只會使用一個CPU或者一條收集線程去完成垃圾收集工作。
- 它在進行垃圾收集時,必須暫停其他所有的工作線程,知道收集結束。
- 適用于Client。
- 新生代使用復制算法,暫停所有線程;老年代使用標記-整理算法,暫停所有線程。
4.2. ParNew收集器
Serial的多線程版本,除了使用多條線程進行垃圾收集之外,其余行為包括Serial收集器的可用參數、收集算法、Stop The World、對象分配規則、回收策略都與Serial收集器完全一樣
-
除了Serial收集器外,目前只有它能與CMS收集器配合工作。
ParNew收集器也是使用
-XX:+UseConcMarkSweepGC
選項后的默認新生代收集器,也可以使用-XX:+UseParNewGC
選項強制指定。ParNew在單核下不會比Serial收集器效果好
可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。
4.3. Parallel Scavenge收集器
他是一個新生代處理器,也是使用復制算法的收集器,也是并行的多線程處理器。
-
Parallel Scavenge收集器的目的是達到一個可控制的吞吐量。
吞吐量 = 運行用戶代碼的時間 / (運行用戶代碼時間 + 垃圾收集時間)
-
它提供了兩個參數控制吞吐量:控制最大垃圾收集停頓的時間-XX:MaxGCPauseMillis,直接設置吞吐量大小-XX:GCTimeRatio
-XX:MaxGCPauseMillis:允許的值是一個大于0的毫秒數,收集器將盡可能地保證內存回收花費不超過設定值,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的。
-XX:GCTimeRatio:參數的值是大于0小于100的整數,就是垃圾收集時間占總時間的比率,如:19,允許最大的時間就是1/(1+19);99,允許最大的時間就是1/(1+99)
-
Parallel Scavenge參數:-XX:UseAdaptiveSizePolicy
-XX:UseAdaptiveSizePolicy 打開這個參數,就不需要手工指定新生代大小、Eden與Survivor區的比列、晉升老年代對象大小等細節參數。虛擬機會根據當前系統的運行情況收集性能監控,動態調整這些參數以提供最適合的停頓時間和最大吞吐量,這種調節方式稱為GC自適應調整策略(GC Ergonomics)
4.4. Serial Old收集器
- Serial收集器的老年代版,單線程,使用“標記-整理”算法
- 作為CMS收集器的后背元,在并發收集發生Concurrent Mode Failure時使用。
4.5. Parallel Old收集器
- 是Parallel Scavenge收集器的老年代版本。使用多線程和“標記-整理”算法。
- 在注重吞吐量以及CPU資源敏感的場景,可以優先考慮Paralled Scavenge+Parallel Old收集器。
4.6 CMS(Concurrent Mark Swap) 收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。適用于互聯網站和B/S系統的服務端上。并發收集、低停頓。
-
CMS收集器是基于“標記-清除”算法實現,過程分為4步:
初始標記(CMS initial mark):僅僅是標記一下GC Roots能直接關聯到的對象,速度很快。
并發標記(CMS concurrent mark):進行GC Roots Tracing的過程。
重新標記(CMS remark):是為了修正并發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄。停頓時間一般比初始標記更長,遠比并發標記時間短。
并發清除(CMS concurrent sweep)
其中初始標記、重新標記這兩個步驟仍然需要“stop the world”
-
CMS的幾個缺點:
對CPU資源非常敏感:它雖然不會導致用戶線程停頓,但是啟用線程,消耗CPU運算資源,會導致引用程序變慢,總吞吐量降低。CMS的默認啟用回收的線程數是(CPU數量 + 3)/ 4.也就是說,CPU越少,占用性能越多,對程序的影響就越大。為了應對這種狀況,JVM提供了“增量式并發收集器”(Incremental Concurrent Mark Swap/i-CMS),使用搶占式來模擬多任務機制,在并發標記和清理的時候讓GC線程、用戶線程交替運行。盡量減少GC線程獨占資源的時間,這樣整個垃圾收集時間過程會更長,但是對用戶的影響就顯得更少。
CMS無法處理“浮動垃圾(Floating Garbage)”,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。浮動垃圾即在CMS并發清理時用戶線程還在運行產生的心垃圾,這部分垃圾出現在標記過后,無法再當次處理。正因為用戶線程還在運行,就需要預留一部分內存給用戶線程使用,所以CMS可以設置觸發百分比:
-XX:CMSInitiatingOccupancyFraction=70
和-XX:+UseCMSInitiatingOccupancyOnly
前者設置百分比,后者設置只用設置的百分比,不讓JVM自動調整,如果不設置后面的,第一次會使用70,隨后就會隨JVM自動調整了。如果CMS運行時,預留內存無法滿足需要,就會出現“Concurrent Mode Failure”,這是JVM就會啟用后后備方案使用Serial Old來重新進行老年代收集。所以比例不能設置太高,不然就會容易引起Concurrent Mode Failure,性能反而降低。CMS是基于“標記-清除”算法實現的,所以收集結束后會有大量的空間碎片產生。雖然空間很多,但是無法給大對象找到一片連續的空間,從而不得不觸發一次Full GC。為了解決這個問題,CMS提供了一個-XX:+UseCMSCompactAtFullCollection,用于在CMS要進行Full GC的時候開啟內存碎片合并整理,這個過程無法并發進行,空間碎片問題解決,但是停頓時間變長。CMS還有一個-XX:CMSFullGCsBeforeCompaction,這個參數是用于設置執行多少次不壓縮的Full GC后,跟著來一次帶壓縮的,為0是表示每次進入Full GC 都壓縮。
4.7 G1(Garbage-First)收集器
-
G1是一款面向服務端應用的垃圾收集器。HotSpot開發來替代CMS的,特點如下:
并行與并發: G1能充分利用多CPU、多核環境下的硬件優勢,使用多個CPU來縮短Stop-The-World停頓的時間,部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過并發的方式讓Java程序繼續執行。
分代收集: 分代概念在G1中依然得以保留。G1可以不需要其他收集器配合就能獨立管理整個GC堆,它能夠采用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次GC的舊對象以獲取更好的收集效果。G1可以自己管理新生代和老年代。
可預測的停頓: 降低停頓時間是G1和CMS共同的關注點,G1除了追求低停頓外,還建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特征了。G1可以有計劃的避免在整個JVM堆中進行垃圾收集,可以對每個region里的回收對象價值(回收該區域的時間消耗和能得到的內存比值)進行分析,在最后篩選回收階段,對每個region里的回收對象價值(回收該區域的時間消耗和能得到的內存比值)最后進行排序,用戶可以自定義停頓時間,那么G1就可以對部分的region進行回收!這使得停頓時間是用戶自己可以控制的!
空間整合,沒有內存碎片產生:由于G1使用了獨立區域(Region)概念,G1從整體來看是基于“標記-整理”算法實現收集,從局部(兩個Region)上來看是基于“復制”算法實現的,但無論如何,這兩種算法都意味著G1運作期間不會產生內存空間碎片。
在G1之前的其他收集器進行收集的范圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的內存布局就與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。
G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在后臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內可以獲取盡可能高的收集效率。
G1收集器中,Region之間的對象引用以及其他收集器中的新生代與老年代之間的對象引用,虛擬機都是使用Remembered Set來避免全堆掃描的。G1中每個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對Reference類型的數據進行寫操作時,會產生一個Write Barrier暫時中斷寫操作,檢查Reference引用的對象是否處于不同的Region之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),如果是,便通過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set之中。當進行內存回收時,在GC根節點的枚舉范圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏。
-
不計算維護Remembered Set的操作,G1收集器的運作大致可劃分為以下幾個步驟:
初始標記(Initial Marking)
并發標記(Concurrent Marking)
最終標記(Final Marking)
篩選回收(Live Data Counting and Evacuation)
關于我
- 坐標杭州,普通本科在讀,計算機科學與技術專業,20年畢業,目前處于實習階段。
- 主要做Java開發,會寫點Golang、Shell。對微服務、大數據比較感興趣,預備做這個方向。
- 目前處于菜鳥階段,各位大佬輕噴,小弟正在瘋狂學習。
- 歡迎大家和我交流鴨!!!