您好,登錄后才能下訂單哦!
本篇內容主要講解“有哪些關于JVM問題”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“有哪些關于JVM問題”吧!
這個問題的前置條件是你得知道 GC 分代,為什么分代。這個在之前文章提了,不清楚的可以去看看。
現在我們來回答一下這個問題。
其實 GC 分為兩大類,分別是 Partial GC 和 Full GC。
Partial GC 即部分收集,分為 young gc、old gc、mixed gc。
young gc:指的是單單收集年輕代的 GC。
old gc:指的是單單收集老年代的 GC。
mixed gc:這個是 G1 收集器特有的,指的是收集整個年輕代和部分老年代的 GC。
Full GC 即整堆回收,指的是收取整個堆,包括年輕代、老年代,如果有永久代的話還包括永久代。
其實還有 Major GC 這個名詞,在《深入理解Java虛擬機》中這個名詞指代的是單單老年代的 GC,也就是和 old gc 等價的,不過也有很多資料認為其是和 full gc 等價的。
還有 Minor GC,其指的就是年輕代的 gc。
大致上可以認為在年輕代的 eden 快要被占滿的時候會觸發 young gc。
為什么要說大致上呢?因為有一些收集器的回收實現是在 full gc 前會讓先執行以下 young gc。
比如 Parallel Scavenge,不過有參數可以調整讓其不進行 young gc。
可能還有別的實現也有這種操作,不過正常情況下就當做 eden 區快滿了即可。
eden 快滿的觸發因素有兩個,一個是為對象分配內存不夠,一個是為 TLAB 分配內存不夠。
這個觸發條件稍微有點多,我們來看下。
在要進行 young gc 的時候,根據之前統計數據發現年輕代平均晉升大小比現在老年代剩余空間要大,那就會觸發 full gc。
有永久代的話如果永久代滿了也會觸發 full gc。
老年代空間不足,大對象直接在老年代申請分配,如果此時老年代空間不足則會觸發 full gc。
擔保失敗即 promotion failure,新生代的 to 區放不下從 eden 和 from 拷貝過來對象,或者新生代對象 gc 年齡到達閾值需要晉升這兩種情況,老年代如果放不下的話都會觸發 full gc。
執行 System.gc()、jmap -dump 等命令會觸發 full gc。
這個得從內存申請說起。
一般而言生成對象需要向堆中的新生代申請內存空間,而堆又是全局共享的,像新生代內存又是規整的,是通過一個指針來劃分的。
內存是緊湊的,新對象創建指針就右移對象大小 size 即可,這叫指針加法(bump [up] the pointer)。
可想而知如果多個線程都在分配對象,那么這個指針就會成為熱點資源,需要互斥那分配的效率就低了。
于是搞了個 TLAB(Thread Local Allocation Buffer),為一個線程分配的內存申請區域。
這個區域只允許這一個線程申請分配對象,允許所有線程訪問這塊內存區域。
TLAB 的思想其實很簡單,就是劃一塊區域給一個線程,這樣每個線程只需要在自己的那畝地申請對象內存,不需要爭搶熱點指針。
當這塊內存用完了之后再去申請即可。
這種思想其實很常見,比如分布式發號器,每次不會一個一個號的取,會取一批號,用完之后再去申請一批。
可以看到每個線程有自己的一塊內存分配區域,短一點的箭頭代表 TLAB 內部的分配指針。
如果這塊區域用完了再去申請即可。
不過每次申請的大小不固定,會根據該線程啟動到現在的歷史信息來調整,比如這個線程一直在分配內存那么 TLAB 就大一些,如果這個線程基本上不會申請分配內存那 TLAB 就小一些。
還有 TLAB 會浪費空間,我們來看下這個圖。
可以看到 TLAB 內部只剩一格大小,申請的對象需要兩格,這時候需要再申請一塊 TLAB ,之前的那一格就浪費了。
在 HotSpot 中會生成一個填充對象來填滿這一塊,因為堆需要線性遍歷,遍歷的流程是通過對象頭得知對象的大小,然后跳過這個大小就能找到下一個對象,所以不能有空洞。
當然也可以通過空閑鏈表等外部記錄方式來實現遍歷。
還有 TLAB 只能分配小對象,大的對象還是需要在共享的 eden 區分配。
所以總的來說 TLAB 是為了避免對象分配時的競爭而設計的。
可以看到和 TLAB 很像,PLAB 即 Promotion Local Allocation Buffers。
用在年輕代對象晉升到老年代時。
在多線程并行執行 YGC 時,可能有很多對象需要晉升到老年代,此時老年代的指針就“熱”起來了,于是搞了個 PLAB。
先從老年代 freelist(空閑鏈表) 申請一塊空間,然后在這一塊空間中就可以通過指針加法(bump the pointer)來分配內存,這樣對 freelist 競爭也少了,分配空間也快了。
大致就是上圖這么個思想,每個線程先申請一塊作為 PLAB ,然后在這一塊內存里面分配晉升的對象。
這和 TLAB 的思想相似。
《深入理解Java虛擬機》:由于CMS收集器無法處理“浮動垃圾”(FloatingGarbage),有可能出現“Con-current Mode Failure”失敗進而導致另一次完全“Stop The World”的Full GC的產生。
這段話的意思是因為拋這個錯而導致一次 Full GC。
而實際上是 Full GC 導致拋這個錯,我們來看一下源碼,版本是 openjdk-8。
首先搜一下這個錯。
再找找看 report_concurrent_mode_interruption
被誰調用。
查到是在 void CMSCollector::acquire_control_and_collect(...)
這個方法中被調用的。
再來看看 first_state : CollectorState first_state = _collectorState;
看枚舉已經很清楚了,就是在 cms gc 還沒結束的時候。
而 acquire_control_and_collect
這個方法是 cms 執行 foreground gc 的。
cms 分為 foreground gc 和 background gc。
foreground 其實就是 Full gc。
因此是 full gc 的時候 cms gc 還在進行中導致拋這個錯。
究其原因是因為分配速率太快導致堆不夠用,回收不過來因此產生 full gc。
也有可能是發起 cms gc 設置的堆的閾值太高。
以下的回答來自 R 大。
因為沒足夠開發資源,偷懶了。就這么簡單。沒有任何技術上的問題。 大公司都自己內部做了優化。
所以最初怎么會偷這個懶的呢?多災多難的CMS GC經歷了多次動蕩。它最初是作為Sun Labs的Exact VM的低延遲GC而設計實現的。
但 Exact VM在與 HotSpot VM爭搶 Sun 的正牌 JVM 的內部斗爭中失利,CMS GC 后來就作為 Exact VM 的技術遺產被移植到了 HotSpot VM上。
就在這個移植還在進行中的時候,Sun 已經開始略顯疲態;到 CMS GC 完全移植到 HotSpot VM 的時候,Sun 已經處于快要不行的階段了。
開發資源減少,開發人員流失,當時的 HotSpot VM 開發組能夠做的事情并不多,只能挑重要的來做。而這個時候 Sun Labs 的另一個 GC 實現,Garbage-First GC(G1 GC)已經面世。
相比可能在長時間運行后受碎片化影響的 CMS,G1 會增量式的整理/壓縮堆里的數據,避免受碎片化影響,因而被認為更具潛力。
于是當時本來就不多的開發資源,一部分還投給了把G1 GC產品化的項目上——結果也是進展緩慢。
畢竟只有一兩個人在做。所以當時就沒能有足夠開發資源去打磨 CMS GC 的各種配套設施的細節,配套的備份 full GC 的并行化也就耽擱了下來。
但肯定會有同學抱有疑問:HotSpot VM不是已經有并行GC了么?而且還有好幾個?
讓我們來看看:
ParNew:并行的young gen GC,不負責收集old gen。
Parallel GC(ParallelScavenge):并行的young gen GC,與ParNew相似但不兼容;同樣不負責收集old gen。
ParallelOld GC(PSCompact):并行的full GC,但與ParNew / CMS不兼容。
所以…就是這么一回事。
HotSpot VM 確實是已經有并行 GC 了,但兩個是只負責在 young GC 時收集 young gen 的,這倆之中還只有 ParNew 能跟 CMS 搭配使用;
而并行 full GC 雖然有一個 ParallelOld,但卻與 CMS GC 不兼容所以無法作為它的備份 full GC使用。
這張圖是 2008 年 HostSpot 一位 GC 組成員畫的,那時候 G1 還沒問世,在研發中,所以畫了個問號在上面。
里面的回答是 :
"ParNew" is written in a style... "Parallel Old" is not written in the "ParNew" style
HotSpot VM 自身的分代收集器實現有一套框架,只有在框架內的實現才能互相搭配使用。
而有個開發他不想按照這個框架實現,自己寫了個,測試的成績還不錯后來被 HotSpot VM 給吸收了,這就導致了不兼容。
我之前看到一個回答解釋的很形象:就像動車組車頭帶不了綠皮車廂一樣,電氣,掛鉤啥的都不匹配。
在常見的分代 GC 中就是利用記憶集來實現的,記錄可能存在的老年代中有新生代的引用的對象地址,來避免全堆掃描。
上圖有個對象精度的,一個是卡精度的,卡精度的叫卡表。
把堆中分為很多塊,每塊 512 字節(卡頁),用字節數組來中的一個元素來表示某一塊,1表示臟塊,里面存在跨代引用。
在 Hotspot 中的實現是卡表,是通過寫后屏障維護的,偽代碼如下。
cms 中需要記錄老年代指向年輕代的引用,但是寫屏障的實現并沒有做任何條件的過濾。
即不判斷當前對象是老年代對象且引用的是新生代對象才會標記對應的卡表為臟。
只要是引用賦值都會把對象的卡標記為臟,當然YGC掃描的時候只會掃老年代的卡表。
這樣做是減少寫屏障帶來的消耗,畢竟引用的賦值非常的頻繁。
cms 的記憶集的實現是卡表即 card table。
通常實現的記憶集是 points-out 的,我們知道記憶集是用來記錄非收集區域指向收集區域的跨代引用,它的主語其實是非收集區域,所以是 points-out 的。
在 cms 中只有老年代指向年輕代的卡表,用于年輕代 gc。
而 G1 是基于 region 的,所以在 points-out 的卡表之上還加了個 points-into 的結構。
因為一個 region 需要知道有哪些別的 region 有指向自己的指針,然后還需要知道這些指針在哪些 card 中。
其實 G1 的記憶集就是個 hash table,key 就是別的 region 的起始地址,然后 value 是一個集合,里面存儲這 card table 的 index。
我們來看下這個圖就很清晰了。
像每次引用字段的賦值都需要維護記憶集開銷很大,所以 G1 的實現利用了 logging write barrier(下文會介紹)。
也是異步思想,會先將修改記錄到隊列中,當隊列超過一定閾值由后臺線程取出遍歷來更新記憶集。
G1 分了 young GC 和 mixed gc。
young gc 會選取所有年輕代的 region 進行收集。
midex gc 會選取所有年輕代的 region 和一些收集收益高的老年代 region 進行收集。
所以年輕代的 region 都在收集范圍內,所以不需要額外記錄年輕代到老年代的跨代引用。
之前文章分析到了并發執行漏標的兩個充分必要條件是:
將新對象插入已掃描完畢的對象中,即插入黑色對象到白色對象的引用。
刪除了灰色對象到白色對象的引用。
cms 和 g1 分別通過增量更新和 SATB 來打破這兩個充分必要條件,維持了 GC 線程與應用線程并發的正確性。
cms 用了增量更新(Incremental update),打破了第一個條件,通過寫屏障將插入的白色對象標記成灰色,即加入到標記棧中,在 remark 階段再掃描,防止漏標情況。
G1 用了 SATB(snapshot-at-the-beginning),打破了第二個條件,會通過寫屏障把舊的引用關系記下來,之后再把舊引用關系再掃描過。
這個從英文名詞來看就已經很清晰了。講白了就是在 GC 開始時候如果對象是存活的就認為其存活,等于拍了個快照。
而且 gc 過程中新分配的對象也都認為是活的。每個 region 會維持 TAMS (top at mark start)指針,分別是 prevTAMS 和 nextTAMS 分別標記兩次并發標記開始時候 Top 指針的位置。
Top 指針就是 region 中最新分配對象的位置,所以 nextTAMS 和 Top 之間區域的對象都是新分配的對象都認為其是存活的即可。
而利用增量更新的 cms 在 remark 階段需要重新所有線程棧和整個年輕代,因為等于之前的根有新增,所以需要重新掃描過,如果年輕代的對象很多的話會比較耗時。
要注意這階段是 STW 的,很關鍵,所以 CMS 也提供了一個 CMSScavengeBeforeRemark 參數,來強制 remark 階段之前來一次 YGC。
而 g1 通過 SATB 的話在最終標記階段只需要掃描 SATB 記錄的舊引用即可,從這方面來說會比 cms 快,但是也因為這樣浮動垃圾會比 cms 多。
寫屏障其實耗的是應用程序的性能,是在引用賦值的時候執行的邏輯,這個操作非常的頻繁,因此就搞了個 logging write barrier。
把寫屏障要執行的一些邏輯搬運到后臺線程執行,來減輕對應用程序的影響。
在寫屏障里只需要記錄一個 log 信息到一個隊列中,然后別的后臺線程會從隊列中取出信息來完成后續的操作,其實就是異步思想。
像 SATB write barrier ,每個 Java 線程有一個獨立的、定長的 SATBMarkQueue,在寫屏障里只把舊引用壓入該隊列中。滿了之后會加到全局 SATBMarkQueueSet。
后臺線程會掃描,如果超過一定閾值就會處理,開始 tracing。
在維護記憶集的寫屏障也用了 logging write barrier 。
G1 從大局上看分為兩大階段,分別是并發標記和對象拷貝。
并發標記是基于 STAB 的,可以分為四大階段:
1、初始標記(initial marking),這個階段是 STW 的,掃描根集合,標記根直接可達的對象即可。在G1中標記對象是利用外部的bitmap來記錄,而不是對象頭。
2、并發階段(concurrent marking),這個階段和應用線程并發,從上一步標記的根直接可達對象開始進行 tracing,遞歸掃描所有可達對象。 STAB 也會在這個階段記錄著變更的引用。
3、最終標記(final marking), 這個階段是 STW 的,處理 STAB 中的引用。
4、清理階段(clenaup),這個階段是 STW 的,根據標記的 bitmap 統計每個 region 存活對象的多少,如果有完全沒存活的 region 則整體回收。
對象拷貝階段(evacuation),這個階段是 STW 的。
根據標記結果選擇合適的 reigon 組成收集集合(collection set 即 CSet),然后將 CSet 存活對象拷貝到新 region 中。
G1 的瓶頸在于對象拷貝階段,需要花較多的瓶頸來轉移對象。
其實從之前問題的 CollectorState 枚舉可以得知幾個流程了。
1、初始標記(initial mark),這個階段是 STW 的,掃描根集合,標記根直接可達的對象即可。
2、并發標記(Concurrent marking),這個階段和應用線程并發,從上一步標記的根直接可達對象開始進行 tracing,遞歸掃描所有可達對象。
3、并發預清理(Concurrent precleaning),這個階段和應用線程并發,就是想幫重新標記階段先做點工作,掃描一下卡表臟的區域和新晉升到老年代的對象等,因為重新標記是 STW 的,所以分擔一點。
4、可中斷的預清理階段(AbortablePreclean),這個和上一個階段基本上一致,就是為了分擔重新標記標記的工作。
5、重新標記(remark),這個階段是 STW 的,因為并發階段引用關系會發生變化,所以要重新遍歷一遍新生代對象、Gc Roots、卡表等,來修正標記。
6、并發清理(Concurrent sweeping),這個階段和應用線程并發,用于清理垃圾。
7、并發重置(Concurrent reset),這個階段和應用線程并發,重置 cms 內部狀態。
cms 的瓶頸就在于重新標記階段,需要較長花費時間來進行重新掃描。
卡表其實只有一份,又得用來支持 YGC 又得支持 CMS 并發時的增量更新肯定是不夠的。
每次 YGC 都會掃描重置卡表,這樣增量更新的記錄就被清理了。
所以還搞了個 mod-union table,在并發標記時,如果發生 YGC 需要重置卡表的記錄時,就會更新 mod-union table 對應的位置。
這樣 cms 重新標記階段就能結合當時的卡表和 mod-union table 來處理增量更新,防止漏標對象了。
分別是最短暫停時間和吞吐量。
最短暫停時間:因為 GC 會 STW 暫停所有應用線程,這時候對于用戶而言就等于卡頓了,因此對于時延敏感的應用來說減少 STW 的時間是關鍵。
吞吐量:對于一些對時延不敏感的應用比如一些后臺計算應用來說,吞吐量是關注的重點,它們不關注每次 GC 停頓的時間,只關注總的停頓時間少,吞吐量高。
舉個例子:
方案一:每次 GC 停頓 100 ms,每秒停頓 5 次。
方案二:每次 GC 停頓 200 ms,每秒停頓 2 次。
兩個方案相對而言第一個時延低,第二個吞吐高,基本上兩者不可兼得。
所以調優時候需要明確應用的目標。
這個問題在面試中很容易問到,抓住核心回答。
現在都是分代 GC,調優的思路就是盡量讓對象在新生代就被回收,防止過多的對象晉升到老年代,減少大對象的分配。
需要平衡分代的大小、垃圾回收的次數和停頓時間。
需要對 GC 進行完整的監控,監控各年代占用大小、YGC 觸發頻率、Full GC 觸發頻率,對象分配速率等等。
然后根據實際情況進行調優。
比如進行了莫名其妙的 Full GC,有可能是某個第三方庫調了 System.gc。
Full GC 頻繁可能是 CMS GC 觸發內存閾值過低,導致對象分配不過來。
還有對象年齡晉升的閾值、survivor 過小等等,具體情況還是得具體分析,反正核心是不變的。
到此,相信大家對“有哪些關于JVM問題”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。