java虛擬機筆記(四)垃圾收集器和策略

4.垃圾收集器和策略
java區別於c++的最大不同點,java擁有動態內存分配和垃圾收集技術。
棧中的區域的數據,比如說局部變量,隨着線程消亡,自然會釋放。
但是堆中的對象則不然,這部分內存的分配和回收都是動態的,垃圾收集器也是關注堆中的內存回收。

垃圾回收時有三個要素要考慮:
1.哪些內存需要回收
2.什麼時候回收
3.如何回收

1.哪些內存需要回收
需要判斷出死亡的對象,死亡的對象是指不可能再被任何途徑使用的對象。

引用計數法:
對象上維護一個引用計數器,有引用時計數器+1,引用失效時計數器-1 ,計數器爲0說明沒有任何對象引用它,那麼就可以回收了。
缺點是會發生循環引用,對象A引用B,對象B引用A,但是實際上A和B都已經沒人去使用了,但是A B的引用計數器不是1,無法被回收。

可達性分析:
當前主流的虛擬機使用的方式。通過一系列GC roots的對象,作爲起始節點,根據引用關係進行搜索,搜索過程就是引用鏈,
如果某個對象和GC roots沒有任何引用鏈關聯,則表示這個對象不可達,說明可以回收。

可作爲Gc roots的對象 :
1.棧中引用的對象
2.方法區類靜態屬性引用的對象
3.方法區常量引用的對象,比如說常量池裏面的字符串
4.所有被同步鎖 synchronized關鍵字修飾的對象
5.局部回收的時候,還要考慮在回收區域之後的其他對象

2.什麼時候回收
java對reference類型的定義,一個reference類型的數據中存儲的數值是另外一塊內存的地址。
reference類型佔用4個字節,比如說 Object a=new Objeact(). 這個a 就是引用類型。

但是 這樣對於對象來說,只能分爲有引用和沒有引用兩種,沒有引用的無法訪問,也就是可以回收。
在jdk1.2之後,拓展了引用的概念 來滿足更多場景的需求,比如說 有些對象 內存足夠的時候不回收,如果內存不足了,就回收,很多緩存的場景會用到。

拓展之後 ,分爲
1.強引用,就是正常的引用,只要存在強引用,就不會被回收,如果內存不足,就拋內存溢出的異常。
2.軟引用 SoftReference,如果內存不足,就回收掉只擁有軟引用的對象
3.弱引用 WeekReference,只擁有弱引用的話,下一次垃圾收集的時候就會回收掉。
4.虛引用 PhantomReference,並不能通過虛引用來訪問一個對象,存在的意義是當被虛引用的對象被回收的時候,系統會發送一個通知。

finalize()方法

這是一個不推薦使用的方法,java虛擬機中有一個finalize線程 回去執行這個方法。 具體流程就是,
當對象被判定爲不可達時,會被標記,然後第一次判斷這個對象是否重寫了finalize方法,finalize方法是否執行過,
如果重寫了,又沒執行過,那麼這個對象會被加入到F_Queue隊列中,然後由一個低調度優先級的finalize線程去執行。
然後收集器會對F_Queue隊列上的對象進行第二次標記,如果finanlize方法已經執行過,且仍然沒有與GC-roots有任何關聯
那就說明可以回收了。 因此,如果在finanlize方法裏面將對象重新加入到引用鏈,會避過此次回收。

注意的是:finanlize方法只會被執行一次,即使在finanlize方法中將自己重新掛到了引用鏈上,之後的回收也不再次執行finanlize方法,
不然一執行又掛上去,永遠都有引用,永遠都不會回收。
也不要用finanlize方法來做資源釋放,finalze線程的優先級很低,不曉得什麼時候纔會執行。

回收方法區:
java虛擬機規範中提到可以不在方法區實現垃圾收集,比如說jdk11的ZGC就不支持類卸載。方法區的垃圾收集性價比比較低。
方法區收集的內容:
1.常量池中不再有用的字面量
2.不再被使用的類型

判斷常量池中常量不被使用,即在程序中沒有任何地方有引用到這個常量,常量池中的其他類,方法,字段的符號引用也是類似。
但是判斷不再被使用的類型,需要這個類型沒有任何對象實例存在,這個類型的類加載器都已經被回收,這個類型對象的java.lang.Class對象沒有在任何地方被引用到。 可以看到,滿足這個條件比較苛刻。

如何回收?垃圾收集算法
垃圾收集算法基於分代收集理論。
1.弱分代假說,絕大數對象都是朝生夕滅
2.強分代假說,熬過多次垃圾收集的對象就難以消亡。

基於這兩個理論,將堆區域劃分爲新生代和老年代,初次創建的對象放到新生代,多次回收仍然存活的對象放到老年代, 新生代用更頻繁的頻率去掃描,老年代的掃描頻率可以適當慢點,這樣就可以更兼顧時間開銷和內存回收的效率。

3.跨代引用假說,跨代引用相對同代引用來說僅佔少數
基於這個假說,只需要在新生代維護一個記錄老年代哪塊區域會存在跨代引用,新生代區域回收選取GCroots時,只把老年代包含這一塊區域的GCroots添加去,而不是掃描所有的老年代。 但是要在對象引用關係改變時,額外消耗性能去維護記錄數據的準確性。

根據回收的區域,又分爲:
1.部分收集 partial GC 指不是收集整個堆的收集方式
2.整堆收集Full GC 收集整個java堆和方法區的收集方式

部分收集 又分爲:
1.新生代收集 minorGC / young GC 只收集新生代的垃圾
2.老年代收集 MajorGC/old GC 只收集老年代的垃圾,目前只有CMS會這樣做
3.混合收集 mixedGC 收集整個新生代和部分老年代的垃圾 目前只有G1會這樣收集

垃圾收集算法
1.標記-清除算法,最早的垃圾收集算法,核心思想是先標記所有的可回收的對象,然後清楚。優點是快,需要花費時間標記,清除則快的多,在垃圾回收時候快了,但是缺點就是內存分配就有問題,會留下大量的內存碎片,可能導致下一次內存分配時候找不到足夠大的內存區域,而提前觸發下一次垃圾收集。

2.標記-複製算法,將內存區域分爲兩部分,先標記可回收或者不可回收的對象,然後將可回收的對象移動到另一部分區域,再清空原來的區域。
優點是永遠有一部分區域是連續的,不會有內存碎片,但是缺點就是要移動對象,如果一輪迴收中,大部分對象都是存活的,那麼就要移動大量的對象,
除非一次回收中,只有少部分對象存活,非常適合新生代,因爲新生代的對象 大部分都活不過一次回收。 另一個缺點就是,內存利用率,因爲永遠要空出來一部分內存。 hotspot的策略是 將新生代分爲eden區和2個survivor區,比例爲8:1, 每次分配對象用eden區和另一個survivor區來分配,存活對象移動到另一個survivor區,然後清空現在這個survivor區和eden區。 這樣可以保證有90%的空間可以使用。
萬一遇到回收的對象,過大 以至於放不下到survivor區中,就會觸發分配擔保,將這個對象直接放到老年代。

3.標記-整理算法, 標記-複製算法適合在新生代,因爲新生代對象存活時間短,需要複製的對象很少。 但是對於老年代,一次垃圾回收,可能還是有大量對象存活的情況,就用標記整理算法。 步驟也是先標記存活的對象,然後讓存活對象都向內存空間一端移動,然後清除掉邊界之外的區域。

但是在移動對象的時候,需要暫停用戶線程,會造成用戶線程的停頓。折中的方案是,先用標記清除算法,等到內存空間碎片積累到一定程度,就用標記整理算法。 CMS就是這樣做的,因爲cms更關注延遲時間。
parallel Scavenge關注的是吞吐量 就是利用的標記整理算法。

GCroots的選擇問題
固定可以作爲GC roots的根結點對象是全局性的對象比如說類靜態成員對象,常量池裏面的對象,執行上下文裏的變量(棧中的局部變量)。
所有的垃圾收集器在枚舉根節點的時候都必須暫停用戶線程,可達性分析引用鏈可以併發, 但是尋找GC roots不能,不然無法保證分析結果的準確性。
當類加載進來時,會將類上哪個位置有什麼類型的對象 記錄在OopMap中,垃圾收集器直接掃描OopMap,並不需要從方法區挨個搜索。

但是可以引起OopMap變化的情況下有很多,對象的引用關係變化,如果每次變化都記錄一次OopMap,那麼將消耗大量的性能。
因此只有在安全點纔會記錄OopMap。 因此,垃圾收集時候,也要求所有用戶線程都在安全點停頓下來,安全點一般是在跳轉,方法調用,異常跳轉的地方。 安全點不能選取的太少以至於讓垃圾收集器等的太久,也不能選的太多,導致頻繁寫OopMap增加負擔,選取的標準是 是否具有讓程序長時間執行的特徵,長時間執行 也就是指令序列的複用處。

在垃圾收集時,需要儘快讓所有用戶線程都到安全點停頓下來,有兩種,一種是搶先式中斷,垃圾收集時,將所有用戶線程先停下來,如果有不在安全點的,就喚醒它,然後重新中斷,直到 它運行到安全點,現在幾乎沒有虛擬機這樣做了。
第二種是 主動式中斷,垃圾收集器設置一個標誌,用戶線程在輪詢點去查看標誌是否存在,如果存在,就將自己停頓在最近的安全點,輪詢點就是安全點。此外對象分配的時候也要去查看這個標誌,如果在垃圾收集中,就先不分配對象。

但是還有一種情況, 就是垃圾收集的時候,部分用戶線程在sleep或者blocked狀態,無法響應中斷請求。 因此爲了避免這種情況,sleep或者block的線程在喚醒之後 在出安全區的時候要看垃圾回收是否結束。
安全區域就是這段區域的代碼,不會導致對象引用關係發生變化,sleep blocked 就是在安全區,垃圾收集器在這段區域內開始垃圾收集都是安全的。
當用戶線程進入到安全區域中時,會標識自己進入了安全區,這樣垃圾收集器就不用去管這部分用戶線程了,當線程要離開安全區域時,會檢測垃圾收集是否結束,如果沒有,就等待垃圾收集結束,如果結束了,就繼續執行。

跨代引用的問題
使用記憶集記錄不在同一代的對象引用關係, 是從非收集區域指向收集區域的指針集合。
hotspot使用寫屏障來維護記憶集的狀態, 寫屏障是 在虛擬機層面對 引用類型賦值 進行的aop切面。
在完成這個引用類型賦值的操作的時候,要去更新記憶集,hotspot上也叫卡表。
作用:比如在新生代收集時候,根據記憶集縮小老年代的Gctoos查找範圍,避免跨代引用漏了GcRoots對象。

併發情況下的可達性分析問題
可達性分析,在非併發情況下,停止所有的用戶線程,那麼呈現的就是一個靜態的視圖,在這個視圖裏面從GCroots出發,掃描所有有關聯的對象。
如果掃描不到的就是可以回收的。

但是併發下,對象的引用關係在變動。因此,爲了保證併發下掃描的正確性。 引入了
1.增量更新。
2.原始快照。

增量更新,如果已經被掃描到的對象(黑色對象),新增指向未掃描對象(白色對象)的引用的時候,這個已掃描對象記錄下來,等併發掃描結束之後,
從這個對象出發,再次掃描。 CMS就是通過增量更新來實現併發標記的。

原始快照,當灰色對象(已經被掃描,但是對象中還存在未掃描的引用)刪除白色對象的時候,將這個要刪除的引用記錄下來,等併發掃描結束之後,從這個對象出發,再次掃描。

增量更新和原始快照都是解決的是,併發垃圾收集過程中,用戶線程新增的引用 沒有被垃圾線程納入到引用鏈中,以至於被意外清除的問題。
無法解決併發垃圾收集過程中,用戶線程產生的新的垃圾。 標記的時候 會判斷是存活還是死亡,如果沒有被標記,自然無法知道是不是垃圾,也就無法被回收。

垃圾收集器:
Serial收集器,新生代收集器,單線程的收集器,收集的時候,會暫停所有的用戶線程。
新生代算法 標記-複製 的時候,暫停所有用戶線程
適用場景爲客戶端,以及一些新生代空間較小的應用,這樣即使暫停也不會很久。

ParNew收集器,新生代收集器,是Serial收集器的多線程版本 ,除了在垃圾收集階段是多線程並行回收垃圾,其他特點和Serial收集器一樣。
適用場景是服務端的新生代,除了它是除了Serial收集器之外,唯一可以和CMS收集器配合的新生代收集器。
默認是開啓和處理器核心數一樣多的線程數。
CMS是垃圾收集線程和用戶線程併發執行的,併發垃圾收集器,但是作爲老年代的垃圾收集器,只能和Serial和ParNew收集器配合 。

Parallel Scavenge收集器
paralle Scavenge收集器關注是吞吐量,吞吐量=執行用戶代碼的時間/(執行垃圾收集的時間+執行用戶代碼的時間)
是新生代收集器,算法是標記-複製,也是多線程的。
吞吐量大意味着可以花費更多的時間在處理程序本身的邏輯上
停頓時間短意味着與用戶交互體驗好,CMS則是力爭停頓時間短。Paralle Scanvenge則是力爭吞吐量。
設置參數 -XX:MaxGCPauseMilis參數,設置最大的停頓時間,收集器會盡量保證停頓不超過這個時間,但是是犧牲吞吐量換來的,
如果停頓時間越短,收集的頻率就越大,吞吐量就越低。
設置參數 -XX:GCTimeRatio ,則是設置垃圾收集時間佔總時間的比例。 默認是99。 也就是 允許1%的垃圾收集時間。
除此之外,還提高了一個自適應的參數 -XX:+UseAdaptiveSizePolicy,激活之後 不再需要人工指定新生代大小,eden和survivor區的比例等 ,
由虛擬機根據運行時的情況自動調整。

Serial Old 收集器
是Serial收集器的老年代版本,單線程收集器,使用標記-整理算法。
適用場景爲hotspot的客戶端模式下適用,客戶端堆比較小,停止用戶線程也不會太久。
或者作爲CMS的後備預案,CMS採用標記-清除算法,當內存碎片過多的時候,得用Serial Old 進行一次標記-整理

Parallel Old 收集器
Parallel Scavenge收集器的老年代版本,用的是標記-整理算法。
使用搭配爲 Parallel Scanvenge 新生代收集+Parallel Old 老年代收集 。

CMS收集器
最求低停頓的垃圾收集器,適用於在B/S服務上,用戶肯定是想要儘快得到響應。
步驟如下:
1.初始標記
這一部分要標記所有GCroots的對象,速度很快,這一步需要停頓用戶線程。
2.併發標記
垃圾收集線程和用戶線程併發執行,這段時間是根據GCroots的對象查找引用鏈路,耗時較久
3.重新標記
需要停頓用戶線程, 修正在併發標記階段,用戶線程併發執行造成的對象引用關係變更導致的掃描不準確
修正的方式是用的增量更新。在併發標記階段,如果已經被標記的對象 新增了引用未被標記的對象,那麼就記錄下指向這個新增對象的引用
在重新標記階段以這個已經被標記的對象爲根,再次掃描。
用戶線程執行期間改動的對象引用關係佔少數,因此耗時比較短。

4.併發清除。
清除線程可以和用戶線程一起執行,清除之前標記階段被判斷爲死亡的對象。 因爲清除算法不需要和整理算法一樣,去移動存活的對象,因此可以和用戶線程一起執行。

缺點:
1.消耗資源更多,因爲併發線程更多,會導致有更多的線程在做垃圾回收的事情,特別是在覈心數小的系統上,吞吐量下降,體現的結果就是,停頓雖然變小,但是執行速度也變慢。
曾有增量式併發收集器,就是讓用戶線程和垃圾收集線程交替執行的方式,增加吞吐量又能保持併發,但是事實上效果並不好,已經在jdk9被移除了。
2.無法解決浮動垃圾,在併發標記和併發清理階段,用戶線程還在執行,就會源源不斷產生新的垃圾,這部分死亡對象是出現在當次標記以後,cms無法在當次的回收中清除掉,這就是浮動垃圾。
此外cms因爲要預留一部分空間給用戶線程使用,並不會等到老年代都滿了才進行收集。
jdk5 閾值是68% , jdk6之後,老年代到了96%的時候纔會觸發cms的老年代收集,這時候如果預留的空間不足給新對象分配空間,就會出現併發失敗,就會觸發備用預案,使用Serial Old來收集老年代,這時候會暫停用戶線程了,就會出現停頓時間長。

G1收集器
G1收集器的目標是在可控制的停頓時間內回收垃圾,但並非一味的追求低停頓時間,要回收時間可控,那麼就要控制垃圾回收的量
CMS 也好 還是ParNew 也好,都是按新生代,老年代來收集。 G1則創造性的,將整個堆,劃分成了N個相同大小的Region區域。
垃圾回收的時候,就可以根據每個Region區的垃圾的數量和比例,以及根據設定的停頓時間, 來衡量選擇哪些Region區域回收,
也就是根據垃圾的"價值" 維護一個優先級列表。 這也是G1收集器的名稱由來,Garbage First 。
使用Region區域劃分區域,按優先級回收的方式,能夠保證在有限的時間內,儘可能的回收最多的空間。判斷哪個region更有回收價值,會根據
平時記錄的回收耗時等相關信息,統計衰減平均值,衰減平均值是比平均平均值更能反應最近的情況。

跨代引用的問題
將區域劃分爲多個Region之後,跨區域引用的問題就更明顯了,因此每個Region都要維護一個記憶集,記憶集是一個map的數據結構,key是來自其他region的起始地址,裏面記錄哪個其他區域的對象引用了本區域的對象。 G1的垃圾收集器的佔用的內存因此也比其他垃圾收集器要大一些。

併發的問題
垃圾回收線程和用戶線程併發執行,用戶線程在垃圾回收的期間還是會創建對象,因此region需要預留空間,且在這個期間創建的對象 都會放到
特定的指針區域上,這部分區域被當前垃圾收集默認不回收。

同時爲了防止併發期間,對象引用改變 導致沒有被掃描到,採用的是原始快照的方式,就是 如果已經被掃描的對象刪除了自己尚未被掃描的對象引用,那麼就會記錄下這個引用,然後在併發標記結束之後,以這個對象爲根節點,再次掃描。也就是有刪除對象引用的時候,保存了一個尚未刪除的快照。

收集的步驟:
1.初始標記
標記GCroots能直接關聯到的對象,該階段需要停頓用戶線程,但是時間很短。
2.併發標記
從GCroots開始對對象進行進行可達性性分析,遞歸掃描整個堆的對象圖,找到要回收的對象,但是是可以和用戶線程併發進行。
3.最終標記
對用戶線程進行暫停,時間很短,處理在併發標記期間發生了引用改變的對象。
4.篩選回收
因爲需要移動對象,所以需要暫停用戶線程,選擇需要回收的region 將裏面的存活對象複製到新的Region裏面,然後清空原來的region。
由多個線程並行執行。

對象分配的時候,如果是比較大的對象,會被分配到比較大的Humongous region裏面,這塊區域是連續的。
G1的目標是在可控的時間內,回收儘可能多的內存。

因此這個停頓時間,也不能隨意設定,比如說設定的過短20毫秒,這個期間回收的空間跟不上對象的分配的話,最終還是會觸發full gc 然後長時間停頓。 最合理的是內存回收的效率跟得上對象分配的速度。

G1和cms相比,最大的優點的就是 停頓時間可控,相對CMS來說,吞吐量更高。
且G1從整體來看 是基於標記-整理算法,單個region來看,又是標記-複製算法,都不會產生內存碎片。
cms和G1相比,則是不需要維護許多的跨區引用的信息,內存佔用少。

但是隨着後面技術的發展,G1肯定越來越好,jdk9已經默認使用G1了,替換了Parallel Scanvenge 和Parallel Old。

黑科技-低延遲收集器 10毫秒內的收集器
G1收集器 初始標記,併發標記,最終標記,篩選回收 ,這裏面,除了併發標記不需要停頓用戶線程,剩下三個都需要停頓用戶線程。
篩選回收階段,因爲用的是標記-複製算法, 所以要移動對象,期間就要讓用戶線程停止下來,不然的話,這個期間也會有用戶線程去訪問
對象,就可能出錯。
標記-清除算法,可以不移動對象,因爲只需要直接清除掉已經死亡的對象就行,因此可以和用戶線程併發執行。

shennadoah收集器,支持與用戶線程併發的整理算法,重點在於 移動對象的時候,需要及時更新對象的引用。
步驟如下:
1.初始標記,標記GCroots可以訪問的對象 和G1一樣,需要停頓用戶線程 時間短
2.併發標記,和G1一樣,不需要停頓用戶線程
3.重新標記, 和G1一樣,需要停頓用戶線程,處理標記期間發生引用改變的對象
4.併發回收,最大的區別,不需要停頓用戶線程,將存活對象複製到新的region區域中,但是用戶線程還在運行中,因此還不能清除原來的對象。
因此還需要將原來舊對象的引用,更新成新對象的。
5.初始引用更新,等待所有的併發回收線程都完成了複製的工作
6.併發引用更新,將原來的舊引用裏的地址都更新成新的地址。
7.最終引用更新,將存在於GCroots裏面的舊引用也更新掉
9.併發清理,清理原來的舊對象。

在執行引用更新期間,如果還有用戶線程訪問對象,則會訪問到對象頭上的轉發指針,繼而轉而訪問新對象。
爲什麼要轉發訪問?因爲用戶線程訪問對象可能是想要修改對象的信息,如果不轉發新的對象上,那麼修改就會落到舊對象上,而舊對象在
引用更新之後,就要被清除。

除了併發更新之外,跨域引用的情況也得到了改善,使用了一張全局的矩陣來替代G1之前每個Region都維護一個記憶集。
矩陣就是一張NxM的二維表
RegionA RegionB
RegionC X
RegionD

橫座標是一系列Region區域,縱座標是一個region區域,有引用關係的 ,在座標交叉點會打個標記 ,比如說上圖的RegionA和RegionC
注意 這個垃圾收集器是redHat公司出的,並不是oracle公司,因此 也沒有被集成到oracle中,但是在openjdk12中有

實際性能表現:

吞吐量大幅下降,運行時間是最長的,但是停頓時間有了質的飛躍,仍沒有達到號稱的10毫秒內。
但是shenandoah還會繼續優化迭代。

ZGC收集器

ZGC也是低延遲的收集器,將任意大小堆上收集時間縮小到10毫秒,而且儘可能不影響吞吐量。
但是ZGC和G1以及shenandoah的設計思想差異巨大,shenandoah和G1的關係更密切。
ZGC使用了讀屏障,染色指針,內存多重映射技術等 實現了併發的標記-整理算法。

染色指針:
在64位系統中,一個指針是64位,裏面存儲了內存的地址,理論上可以訪問2的64次分,也就是16EB的空間。
但是實際上並不會用到那麼多,目前64位硬件最多支持最大的內存是256TB 2的48次方。
但是操作系統還有約束,真正可以使用的只有46位(64TB)物理地址。
因此,染色指針將這46位的最後4位拿來存儲額外信息,所以ZGC能管理的內存只有4TB (42位 2的42次方 )
再通過一種叫內存映射的方式,讓操作系統能識別出來這種地址,屏蔽掉額外信息的影響,正常訪問地址,額外信息只給垃圾收集器識別。

不同於shenadoah的轉發指針的設計,ZGC使用了染色指針(tagPointer)。
先想一個問題,什麼東西能判斷一個對象是否存活?對象本身是無法決定自己是否存活的,只有對象的引用關係可以。
那麼對象是否存活的標記,不同的收集器有不同的做法,serial將這個信息存在對象頭上,G1和shenandoah將這個信息存到了一個與對象獨立的數據結構上(bigMap) 。 而ZGC的染色指針,則將標記信息直接存到了指針上,也叫染色指針。 標記信息會告訴垃圾收集器,當前對象是否是在重分配集中(記錄對象是否需要移動位置)。 利用這點,ZGC可以在一個region都完成了對象複製之後,就可以立刻清除這個region。

Shenadoah收集器則是需要等待這個Region的對象複製完之後,且這個堆的引用都更新完 ,才能清除這個region。
因爲它把轉發指針存到了舊對象的對象頭中,如果在引用沒有全部更新之前,清除了舊對象,那麼就沒法訪問新對象了。

ZGC維護了一個重分配集(代表這個對象移動過)和每個region都有一個轉發表,然後在併發標記的階段,發現需要移動的存活對象,就在該對象的引用指針維護了其是否在重分配集上的信息。在併發整理階段,當region裏面的存活對象被複制到了新的region裏面後,轉發表中就會記錄原地址和新地址的信息。當用戶線程訪問這個引用時候,通過原來的引用指針 ,就會發現其在重分配集上,就去這個region的轉發表上去新的region裏面訪問,並刷新這個引用。

特點:在併發標記階段將對象是否需要移動就記錄在了這個對象引用指針上,這樣即使這個對象被清除了
,用這個指針訪問的時候,也可以發現這個對象被移動了,觸發去轉發表上查找新的地址,然後訪問並更新這個引用。

就可以在region存活的對象複製完成之後就立刻清除原來的region,但是轉發表還不能刪。 等到所有舊引用都更新之後,才能刪掉轉發表。

步驟如下:
1.初始標記 從GCroots出發 需要暫停用戶線程
2.併發標記,如果發現需要移動的對象 會將這個信息記錄到這個對象的引用地址上。
3.併發預備分配,將要收集的對象都記錄到重分配集合中。
4.併發重分配,將重分配集中的對象複製到新的region上,併爲原來的region上維護一個轉發表。
如果期間用戶線程訪問對象,那麼可以通過訪問這個對象的引用上的標記,就可以知道這個對象是否是要移動的,
如果是,就去訪問轉發表,通過轉發表得到新的region地址去訪問,並更新這個引用。
這個期間,因爲只需要通過引用即可知道對象是否已經移動,那麼當region的對象都複製之後,就可以直接清空這個region了,不需要等到堆中的引用更新完。 這點比shenadoah收集器要好。
5.併發重映射,將舊引用更新成新的,但是這個任務並非那麼迫切,因爲有轉發表和自更新的存在,即使不去主動更新全部的引用,用戶線程也可以正常訪問移動後的對象並更新最新的引用地址。 因此這個階段通常會被合併到下一次垃圾收集的併發標記階段去做,因爲併發標記階段也是要訪問所有的對象,隨便就可以更新下自己的引用。

ZGC通過併發標記-整理算法,極大的縮小了用戶線程的停頓時間,同時又通過染色指針 提高了回收效率,保證了較高的吞吐量, 在jdk12中提供。
測試中的效果是 停頓時間毫秒級,但是吞吐量達到了以吞吐量爲目標的parallel scavenge 的99%。

epsilon收集器
不能收集垃圾的收集器。
垃圾收集器的本質是 自動內存管理系統,除了回收垃圾之外,還有着對象分配,內存管理,與編譯器 解釋器 監控子系統的協作等等。 epsilon只是不回收垃圾,其他功能都是正常的。
爲了隔離垃圾收集器與java虛擬機的其他功能 比如說編譯器 解釋器 監控子系統的耦合。
redhat提出了統一垃圾收集器接口, epsilon是這個接口的驗證和參考實現。同時,也應用於需要剝離垃圾收集器影響的性能測試和壓力測試。

同時 生產環境上,如果服務的運行時間極短,能夠在堆耗盡之前就結束的話,也可以用epsilon收集器,運行負載小,沒有任何回收行爲。

垃圾收集器的選擇:
衡量標準的三個要素:
1.吞吐量
如果是數據分析,科學計算類的應用,那麼吞吐量優先,吞吐量是指運行用戶任務時間和總執行時間的比例,吞吐量越大用於執行用戶任務時間越多。
2.延遲時間
面向用戶的應用,延遲時間越短用戶體驗越好 .4到6GB以下的內存 cms處理的很好,如果更大的內存,那麼G1有優勢。
3.內存佔用
客戶端程序 或者嵌入式應用 那麼需要考慮垃圾收集器的內存佔用

jdk9將垃圾收集器的日誌配置給統一了。

內存分配與回收策略
1.對象優先在eden區分配
新創建對象 優先在eden區創建,如果eden區不夠了,就會觸發minorGC 新生代回收
將eden存活的對象移動到乾淨的survivor區去,然後清空eden區和另一個survivor區。
-Xms 最小堆空間 -Xmx 最大堆空間 -xmn 年輕代空間 -XX:SurvivorRatio=8 eden區和survivor區比例

2.大對象直接進入老年代
-XX pretenureSizeThreshold 指定超過這個值大小的對象 就會直接分配到老年代。
這樣設計的目的是,大對象的複製是極其昂貴的,如果放到新生代,隨着垃圾收集複製來複制去,很浪費性能。還有就是,佔據了新生代大量的空間,會導致提前觸發minorGC ,
但是 pretenurseSizeThreshold 只對Serial和ParNew兩個收集器有效。

3.長期存活的對象進入老年代
對象在經過一次minorGC之後計數+1,15次之後 就會被放到老年代。GC次數維護在對象頭上。

4.動態對象年齡判定
並非一定要求要GC15次之後纔會被放到老年代,但是survivor區裏面的對象,相同年齡的所有對象大小總和大於Survivor空間的一半,大於或者等於這個年齡的對象就會被放到老年代。

5.空間分配擔保。
擔保的是minorGC, 用老年代來兜底。 因爲minorGC可能出現Survivor空間不夠的情況。
這個時候,就需要講一部分對象移動到老年代。
因此,每次minorCG之前,都要去檢查老年代有沒有足夠大的連續空間可以放下新生代所有對象。
如果有,那麼這次minorGC就是安全的,因爲即使survivor空間不夠,也可以直接講對象移動到老年代。
如果沒有,那麼就看老年代的有沒有足夠大的連續空間大於歷次晉升老年代對象的平均大小。
如果有,那就嘗試minorGC,如果失敗,觸發full GC。
如果沒有,就fullGC 。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章