您好,登錄后才能下訂單哦!
這篇文章主要介紹了怎么用GC算法實現垃圾優先算法的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇怎么用GC算法實現垃圾優先算法文章都會有所收獲,下面我們一起來看看吧。
G1最主要的設計目標是: 將STW停頓的時間和分布變成可預期以及可配置的。事實上, G1是一款軟實時垃圾收集器, 也就是說可以為其設置某項特定的性能指標. 可以指定: 在任意 xx
毫秒的時間范圍內, STW停頓不得超過 x
毫秒。 如: 任意1秒暫停時間不得超過5毫秒. Garbage-First GC 會盡力達成這個目標(有很大的概率會滿足, 但并不完全確定,具體是多少將是硬實時的[hard real-time])。
為了達成這項指標, G1 有一些獨特的實現。首先, 堆不再分成連續的年輕代和老年代空間。而是劃分為多個(通常是2048個)可以存放對象的 小堆區(**aller heap regions)。每個小堆區都可能是 Eden區, Survivor區或者Old區. 在邏輯上, 所有的Eden區和Survivor區合起來就是年輕代, 所有的Old區拼在一起那就是老年代:
這樣的劃分使得 GC不必每次都去收集整個堆空間, 而是以增量的方式來處理: 每次只處理一部分小堆區,稱為此次的回收集(collection set). 每次暫停都會收集所有年輕代的小堆區, 但可能只包含一部分老年代小堆區:
G1的另一項創新, 是在并發階段估算每個小堆區存活對象的總數。用來構建回收集(collection set)的原則是: 垃圾最多的小堆區會被優先收集。這也是G1名稱的由來: garbage-first。
要啟用G1收集器, 使用的命令行參數為:
java -XX:+UseG1GC com.mypackages.MyExecutableClass
在應用程序剛啟動時, G1還未執行過(not-yet-executed)并發階段, 也就沒有獲得任何額外的信息, 處于初始的 fully-young 模式. 在年輕代空間用滿之后, 應用線程被暫停, 年輕代堆區中的存活對象被復制到存活區, 如果還沒有存活區,則選擇任意一部分空閑的小堆區用作存活區。
復制的過程稱為轉移(Evacuation), 這和前面講過的年輕代收集器基本上是一樣的工作原理。轉移暫停的日志信息很長,為簡單起見, 我們去除了一些不重要的信息. 在并發階段之后我們會進行詳細的講解。此外, 由于日志記錄很多, 所以并行階段和“其他”階段的日志將拆分為多個部分來進行講解:
0.134: [GC pause (G1 Evacuation Pause) (young), 0.0144119 secs] [Parallel Time: 13.9 ms, GC Workers: 8] … [Code Root Fixup: 0.0 ms] [Code Root Purge: 0.0 ms] [Clear CT: 0.1 ms] [Other: 0.4 ms] … [Eden: 24.0M(24.0M)->0.0B(13.0M) Survivors: 0.0B->3072.0K Heap: 24.0M(256.0M)->21.9M(256.0M)] [Times: user=0.04 sys=0.04, real=0.02 secs]
>
0.134: [GC pause (G1 Evacuation Pause) (young), 0.0144119 secs]
– G1轉移暫停,只清理年輕代空間。暫停在JVM啟動之后 134 ms 開始, 持續的系統時間為 0.0144秒 。
[Parallel Time: 13.9 ms, GC Workers: 8]
– 表明后面的活動由8個 Worker 線程并行執行, 消耗時間為13.9毫秒(real time)。
…
– 為閱讀方便, 省略了部分內容,請參考后文。
[Code Root Fixup: 0.0 ms]
– 釋放用于管理并行活動的內部數據。一般都接近于零。這是串行執行的過程。
[Code Root Purge: 0.0 ms]
– 清理其他部分數據, 也是非常快的, 但如非必要則幾乎等于零。這是串行執行的過程。
[Other: 0.4 ms]
– 其他活動消耗的時間, 其中有很多是并行執行的。
…
– 請參考后文。
[Eden: 24.0M(24.0M)->0.0B(13.0M)
– 暫停之前和暫停之后, Eden 區的使用量/總容量。
Survivors: 0.0B->3072.0K
– 暫停之前和暫停之后, 存活區的使用量。
Heap: 24.0M(256.0M)->21.9M(256.0M)]
– 暫停之前和暫停之后, 整個堆內存的使用量與總容量。
[Times: user=0.04 sys=0.04, real=0.02 secs]
– GC事件的持續時間, 通過三個部分來衡量:
user – 在此次垃圾回收過程中, 由GC線程所消耗的總的CPU時間。
sys – GC過程中, 系統調用和系統等待事件所消耗的時間。
real – 應用程序暫停的時間。在并行GC(Parallel GC)中, 這個數字約等于: (user time + system time)/GC線程數。 這里使用的是8個線程。 請注意,總是有一定比例的處理過程是不能并行化的。
說明: 系統時間(wall clock time, elapsed time), 是指一段程序從運行到終止,系統時鐘走過的時間。一般來說,系統時間都是要大于CPU時間
最繁重的GC任務由多個專用的 worker 線程來執行。下面的日志描述了他們的行為:
[Parallel Time: 13.9 ms, GC Workers: 8] [GC Worker Start (ms): Min: 134.0, Avg: 134.1, Max: 134.1, Diff: 0.1] [Ext Root Scanning (ms): Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.2, Sum: 1.2] [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0] [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.2, Diff: 0.2, Sum: 0.2] [Object Copy (ms): Min: 10.8, Avg: 12.1, Max: 12.6, Diff: 1.9, Sum: 96.5] [Termination (ms): Min: 0.8, Avg: 1.5, Max: 2.8, Diff: 1.9, Sum: 12.2] [Termination Attempts: Min: 173, Avg: 293.2, Max: 362, Diff: 189, Sum: 2346] [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1] GC Worker Total (ms): Min: 13.7, Avg: 13.8, Max: 13.8, Diff: 0.1, Sum: 110.2] [GC Worker End (ms): Min: 147.8, Avg: 147.8, Max: 147.8, Diff: 0.0]
[Parallel Time: 13.9 ms, GC Workers: 8]
– 表明下列活動由8個線程并行執行,消耗的時間為13.9毫秒(real time)。
[GC Worker Start (ms)
– GC的worker線程開始啟動時,相對于 pause 開始的時間戳。如果 Min
和 Max
差別很大,則表明本機其他進程所使用的線程數量過多, 擠占了GC的CPU時間。
[Ext Root Scanning (ms)
– 用了多長時間來掃描堆外(non-heap)的root, 如 classloaders, JNI引用, JVM的系統root等。后面顯示了運行時間, “Sum” 指的是CPU時間。
[Code Root Scanning (ms)
– 用了多長時間來掃描實際代碼中的 root: 例如局部變量等等(local vars)。
[Object Copy (ms)
– 用了多長時間來拷貝收集區內的存活對象。
[Termination (ms)
– GC的worker線程用了多長時間來確保自身可以安全地停止, 這段時間什么也不用做, stop 之后該線程就終止運行了。
[Termination Attempts
– GC的worker 線程嘗試多少次 try 和 teminate。如果worker發現還有一些任務沒處理完,則這一次嘗試就是失敗的, 暫時還不能終止。
[GC Worker Other (ms)
– 一些瑣碎的小活動,在GC日志中不值得單獨列出來。
GC Worker Total (ms)
– GC的worker 線程的工作時間總計。
[GC Worker End (ms)
– GC的worker 線程完成作業的時間戳。通常來說這部分數字應該大致相等, 否則就說明有太多的線程被掛起, 很可能是因為壞鄰居效應(noisy neighbor) 所導致的。
此外,在轉移暫停期間,還有一些瑣碎執行的小活動。這里我們只介紹其中的一部分, 其余的會在后面進行討論。
[Other: 0.4 ms] [Choose CSet: 0.0 ms] [Ref Proc: 0.2 ms] [Ref Enq: 0.0 ms] [Redirty Cards: 0.1 ms] [Humongous Register: 0.0 ms] [Humongous Reclaim: 0.0 ms] [Free CSet: 0.0 ms]
[Other: 0.4 ms]
– 其他活動消耗的時間, 其中有很多也是并行執行的。
[Ref Proc: 0.2 ms]
– 處理非強引用(non-strong)的時間: 進行清理或者決定是否需要清理。
[Ref Enq: 0.0 ms]
– 用來將剩下的 non-strong 引用排列到合適的 ReferenceQueue中。
[Free CSet: 0.0 ms]
– 將回收集中被釋放的小堆歸還所消耗的時間, 以便他們能用來分配新的對象。
G1收集器的很多概念建立在CMS的基礎上,所以下面的內容需要你對CMS有一定的理解. 雖然也有很多地方不同, 但并發標記的目標基本上是一樣的. G1的并發標記通過 Snapshot-At-The-Beginning(開始時快照) 的方式, 在標記階段開始時記下所有的存活對象。即使在標記的同時又有一些變成了垃圾. 通過對象是存活信息, 可以構建出每個小堆區的存活狀態, 以便回收集能高效地進行選擇。
這些信息在接下來的階段會用來執行老年代區域的垃圾收集。在兩種情況下是完全地并發執行的: 一、如果在標記階段確定某個小堆區只包含垃圾; 二、在STW轉移暫停期間, 同時包含垃圾和存活對象的老年代小堆區。
當堆內存的總體使用比例達到一定數值時,就會觸發并發標記。默認值為 45%
, 但也可以通過JVM參數 InitiatingHeapOccupancyPercent
來設置。和CMS一樣, G1的并發標記也是由多個階段組成, 其中一些是完全并發的, 還有一些階段需要暫停應用線程。
階段 1: Initial Mark(初始標記)。 此階段標記所有從GC root 直接可達的對象。在CMS中需要一次STW暫停, 但G1里面通常是在轉移暫停的同時處理這些事情, 所以它的開銷是很小的. 可以在 Evacuation Pause 日志中的第一行看到(initial-mark)暫停:
1.631: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0062656 secs]
階段 2: Root Region Scan(Root區掃描). 此階段標記所有從 “根區域” 可達的存活對象。 根區域包括: 非空的區域, 以及在標記過程中不得不收集的區域。因為在并發標記的過程中遷移對象會造成很多麻煩, 所以此階段必須在下一次轉移暫停之前完成。如果必須啟動轉移暫停, 則會先要求根區域掃描中止, 等它完成才能繼續掃描. 在當前版本的實現中, 根區域是存活的小堆區: y包括下一次轉移暫停中肯定會被清理的那部分年輕代小堆區。
1.362: [GC concurrent-root-region-scan-start] 1.364: [GC concurrent-root-region-scan-end, 0.0028513 secs]
階段 3: Concurrent Mark(并發標記). 此階段非常類似于CMS: 它只是遍歷對象圖, 并在一個特殊的位圖中標記能訪問到的對象. 為了確保標記開始時的快照準確性, 所有應用線程并發對對象圖執行的引用更新,G1 要求放棄前面階段為了標記目的而引用的過時引用。
這是通過使用 Pre-Write
屏障來實現的,(不要和之后介紹的 Post-Write
混淆, 也不要和多線程開發中的內存屏障(memory barriers)相混淆)。Pre-Write屏障的作用是: G1在進行并發標記時, 如果程序將對象的某個屬性做了變更, 就會在 log buffers 中存儲之前的引用。 由并發標記線程負責處理。
1.364: [GC concurrent-mark-start] 1.645: [GC co ncurrent-mark-end, 0.2803470 secs]
階段 4: Remark(再次標記). 和CMS類似,這也是一次STW停頓,以完成標記過程。對于G1,它短暫地停止應用線程, 停止并發更新日志的寫入, 處理其中的少量信息, 并標記所有在并發標記開始時未被標記的存活對象。這一階段也執行某些額外的清理, 如引用處理(參見 Evacuation Pause log) 或者類卸載(class unloading)。
1.645: [GC remark 1.645: [Finalize Marking, 0.0009461 secs] 1.646: [GC ref-proc, 0.0000417 secs] 1.646: [Unloading, 0.0011301 secs], 0.0074056 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
階段 5: Cleanup(清理). 最后這個小階段為即將到來的轉移階段做準備, 統計小堆區中所有存活的對象, 并將小堆區進行排序, 以提升GC的效率. 此階段也為下一次標記執行所有必需的整理工作(house-keeping activities): 維護并發標記的內部狀態。
最后要提醒的是, 所有不包含存活對象的小堆區在此階段都被回收了。有一部分是并發的: 例如空堆區的回收,還有大部分的存活率計算, 此階段也需要一個短暫的STW暫停, 以不受應用線程的影響來完成作業. 這種STW停頓的日志如下:
1.652: [GC cleanup 1213M->1213M(1885M), 0.0030492 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
如果發現某些小堆區中只包含垃圾, 則日志格式可能會有點不同, 如:
1.872: [GC cleanup 1357M->173M(1996M), 0.0015664 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 1.874: [GC concurrent-cleanup-start] 1.876: [GC concurrent-cleanup-end, 0.0014846 secs]
能并發清理老年代中整個整個的小堆區是一種最優情形, 但有時候并不是這樣。并發標記完成之后, G1將執行一次混合收集(mixed collection), 不只清理年輕代, 還將一部分老年代區域也加入到 collection set 中。
混合模式的轉移暫停(Evacuation pause)不一定緊跟著并發標記階段。有很多規則和歷史數據會影響混合模式的啟動時機。比如, 假若在老年代中可以并發地騰出很多的小堆區,就沒有必要啟動混合模式。
因此, 在并發標記與混合轉移暫停之間, 很可能會存在多次 fully-young 轉移暫停。
添加到回收集的老年代小堆區的具體數字及其順序, 也是基于許多規則來判定的。 其中包括指定的軟實時性能指標, 存活性,以及在并發標記期間收集的GC效率等數據, 外加一些可配置的JVM選項. 混合收集的過程, 很大程度上和前面的 fully-young gc 是一樣的, 但這里我們還要介紹一個概念: remembered sets(歷史記憶集)。
Remembered sets (歷史記憶集)是用來支持不同的小堆區進行獨立回收的。例如,在收集A、B、C區時, 我們必須要知道是否有從D區或者E區指向其中的引用, 以確定他們的存活性. 但是遍歷整個堆需要相當長的時間, 這就違背了增量收集的初衷, 因此必須采取某種優化手段. 其他GC算法有獨立的 Card Table 來支持年輕代的垃圾收集一樣, 而G1中使用的是 Remembered Sets。
如下圖所示, 每個小堆區都有一個 remembered set, 列出了從外部指向本區的所有引用。這些引用將被視為附加的 GC root. 注意,在并發標記過程中,老年代中被確定為垃圾的對象會被忽略, 即使有外部引用指向他們: 因為在這種情況下引用者也是垃圾。
接下來的行為,和其他垃圾收集器一樣: 多個GC線程并行地找出哪些是存活對象,確定哪些是垃圾:
最后, 存活對象被轉移到存活區(survivor regions), 在必要時會創建新的小堆區。現在,空的小堆區被釋放, 可用于存放新的對象了。
為了維護 remembered set, 在程序運行的過程中, 只要寫入某個字段,就會產生一個 Post-Write 屏障。如果生成的引用是跨區域的(cross-region),即從一個區指向另一個區, 就會在目標區的Remembered Set中,出現一個對應的條目。為了減少 Write Barrier 造成的開銷, 將卡片放入Remembered Set 的過程是異步的, 而且經過了很多的優化. 總體上是這樣: Write Barrier 把臟卡信息存放到本地緩沖區(local buffer), 有專門的GC線程負責收集, 并將相關信息傳給被引用區的 remembered set。
混合模式下的日志, 和純年輕代模式相比, 可以發現一些有趣的地方:
[[Update RS (ms): Min: 0.7, Avg: 0.8, Max: 0.9, Diff: 0.2, Sum: 6.1] [Processed Buffers: Min: 0, Avg: 2.2, Max: 5, Diff: 5, Sum: 18] [Scan RS (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.8] [Clear CT: 0.2 ms] [Redirty Cards: 0.1 ms]
[Update RS (ms)
– 因為 Remembered Sets 是并發處理的,必須確保在實際的垃圾收集之前, 緩沖區中的 card 得到處理。如果card數量很多, 則GC并發線程的負載可能就會很高。可能的原因是, 修改的字段過多, 或者CPU資源受限。
[Processed Buffers
– 每個 worker 線程處理了多少個本地緩沖區(local buffer)。
[Scan RS (ms)
– 用了多長時間掃描來自RSet的引用。
[Clear CT: 0.2 ms]
– 清理 card table 中 cards 的時間。清理工作只是簡單地刪除“臟”狀態, 此狀態用來標識一個字段是否被更新的, 供Remembered Sets使用。
[Redirty Cards: 0.1 ms]
– 將 card table 中適當的位置標記為 dirty 所花費的時間。”適當的位置”是由GC本身執行的堆內存改變所決定的, 例如引用排隊等。
關于“怎么用GC算法實現垃圾優先算法”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“怎么用GC算法實現垃圾優先算法”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。