摘要:虛擬機所處的區域,則表示它是屬于新生代收集器還是老年代收集器。虛擬機總共運行了分鐘,其中垃圾收集花掉分鐘,那么吞吐量就是。收集器線程所占用的數量為。
本文主要從GC(垃圾回收)的角度試著對jvm中的內存分配策略與相應的垃圾收集器做一個介紹。
注:還是老規矩,本著能畫圖就不BB原則,盡量將各知識點通過思維導圖或者其他模型圖的方式進行說明。文字僅記錄額外的思考與心得,以及其他特殊情況
內存分配策略本部分的回答主要圍繞 哪些內存需要回收?什么時候回收?以及如何回收?這三個問題來進行介紹。
哪些內存需要回收? 一張圖總結 補充說明由上圖可知,只有堆區和靜態區,運行時才能知道創建的對象信息,所以垃圾收集器所需要關注的內存也就集中于這兩個部分了。
什么時候回收? 堆區 回收依據不可能再被任何途徑使用(對象已死)
對象存活判定算法主流對象存過判定算法分為如下兩種:
引用計數算法
可達性分析算法
補充說明在 java 中引用分為強軟弱虛四種形式,
最常見的就是強引用,比如類似Object obj = new Object()這種。
軟引用通過 “SoftReference” 來實現
弱引用通過 “WeakReference” 來實現
弱引用通過 “PhantomReference” 來實現
方法區在方法區中,垃圾收集遠不像堆區那么頻繁和高效。我們聚焦于兩部分內容,廢棄常量和無用的類。
補充介紹針對是否對類進行回收,HotSpot 虛擬機提供了 -Xnoclassgc 參數進行控制
針對類加載和卸載信息,可以使用 -verbose:class 以及 -XX:+TraceClassLoading、-XX:TraceClassUnLoading
注:-verbose:class 以及 -XX:+TraceClassLoading 可以用在Product版的虛擬機中。-XX:+TraceClassUnLoading 參數需要 FastDebug 版的虛擬機支持。
如何回收?其實如何回收也是具體的垃圾收集器該干的的事。但是各個平臺的虛擬機操作內存的方法又各不相同。所以這部分先站在一個略宏觀的角度討論下關于垃圾回收的幾種常見算法。
標記-清除算法 示意圖 一張圖總結 復制收集算法 示意圖: 一張圖總結 拓展說明傳統的復制算法由于將內存劃分為了兩半,導致同一時間內存的可用率只有50%,這顯然是難以接受的。
所以也早就有了機智的前輩對此方法進行了改進,接下來就來介紹下 HotSpot 虛擬機中是如何改進的~
復制收集算法在對象存活率較高的時候,就要進行較多的復制操作,效率將會變低。更關鍵的是,如果不想浪費50%的控件,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況。
所以針對老年代的特點,一般更傾向使用類似“標記-整理”而非“復制收集”這樣的算法。
分代收集算法 一張圖總結 HotSpot 的算法實現難點前面從理論上介紹了對象存活的判定方法和垃圾收集算法的思想,但是具體實現的過程中,也才會發現一些在理論思考時不會注意的點。
枚舉根節點 難點 解決方案通過一組稱為 OopMap 的數據結構來達到目的:
在類加載完成的時候,HotSpot 將對象內數據類型及其偏移量記錄下來
JIT 編譯過程中也在特定的位置記錄下棧和寄存器中哪些位置使引用
通過這種事前約定記錄位置的方法,實現快速遍歷根節點引用
安全點 概念由來安全點的由來本身也是為了解決一個難題而產生的:
位置選定的要點 如何進入安全點 安全區域 垃圾收集器不同的廠商,不同版本的虛擬機所提供的垃圾收集器差別很大,為了方便討論,這里以 JDK 1.7 Update 14 為基礎進行討論。
一張圖總結上圖展示了7種作用于不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用。虛擬機所處的區域,則表示它是屬于新生代收集器還是老年代收集器。
Serial 收集器 運行示意圖(新生代部分) 優缺點分析 ParNew 收集器 運行示意圖(新生代部分) 優缺點分析 補充說明ParNew 默認開啟的垃圾收集器線程數就是CPU數量,可通過-XX:parallelGCThreads參數來限制收集器線程數
另:
從 ParNew 收集器開始,后續還有幾款并發和并行收集器。這里解釋一下這兩個名詞:并發和并行。這兩個名詞都是并發編程中的概念,在談論垃圾收集器的上下文語境中,它們可以解釋如下:
并行(Parallel):指多條垃圾收集線程并行工作,但此時用戶線程仍處于等待狀態。
并發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是并行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行于另一個CPU上。
Parallel Scavenge 收集器 運行示意圖(新生代部分) 優缺點分析 補充說明提供了兩個參數來精確控制吞吐量:
最大垃圾收集器停頓時間(-XX:MaxGCPauseMillis 大于0的毫秒數,停頓時間小了就要犧牲相應的吞吐量和新生代空間),
設置吞吐量大小(-XX:GCTimeRatio 大于0小于100的整數,默認99,也就是允許最大1%的垃圾回收時間)。
還有一個參數表示自適應調節策略(GC Ergonomics)(-XX:UseAdaptiveSizePolicy)。就不用手動設置新生代大?。?Xmn)、Eden和Survivor區的比例(-XX:SurvivorRatio)晉升老年代對象大小(-XX:PretenureSizeThreshold),會根據當前系統的運行情況手機監控信息,動態調整停頓時間和吞吐量大小。也是其與PreNew收集器的一個重要區別,也是其無法與CMS收集器搭配使用的原因(CMS收集器盡可能地縮短垃圾收集時用戶線程的停頓時間,以提升交互體驗)。
另:所謂吞吐量就是CPU用于運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)。
虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那么吞吐量就是99%。
Serial Old 收集器 運行示意圖(老年代部分) 優缺點分析 Parallel Old 收集器 運行示意圖(老年代部分)(圖畫錯了,老年代應該是并行收集才對)
優缺點分析 CMS 收集器 運行示意圖 優缺點分析 補充說明CMS收集器是基于“標記-清除”算法實現的,整個收集過程大致分為4個步驟:
①.初始標記(CMS initial mark)
②.并發標記(CMS concurrenr mark)
③.重新標記(CMS remark)
④.并發清除(CMS concurrent sweep)
其中初始標記、重新標記這兩個步驟任然需要停頓其他用戶線程(Stop The World)。
初始標記僅僅只是標記出 GC ROOTS 能直接關聯到的對象,速度很快,并發標記階段是進行 GC ROOTS 根搜索算法階段,會判定對象是否存活。而重新標記階段則是為了修正并發標記期間,因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間會被初始標記階段稍長,但比并發標記階段要短。
由于整個過程中耗時最長的并發標記和并發清除過程中,收集器線程都可以與用戶線程一起工作,所以整體來說,CMS收集器的內存回收過程是與用戶線程一起并發執行的。
關于CMS的三個缺點,這里有更詳細的解釋說明:
G1 收集器 運行示意圖 優缺點分析 補充說明CMS收集器對CPU資源非常敏感。在并發(并發標記、并發清除)階段,雖然不會導致用戶線程停頓,但是會占用CPU資源而導致應用程序變慢,總吞吐量下降。CMS默認啟動的回收線程數是:(CPU數量+3) / 4。收集器線程所占用的CPU數量為:(CPU+3)/4=0.25+3/(4*CPU)。因此這時垃圾收集器始終不會占用少于25%的CPU,因此當進行并發階段時,雖然用戶線程可以跑,但是很緩慢,特別是雙核CPU的時候,已經占用了5/8的CPU,吞吐量會很低。為了解決這種情況,產生了“增量式并發收集器”(Incremental Concurrent Mark Sweep/i-CMS)。就是采用搶占方式來模擬多任務機制,就是在并發(并發標記、并發清除)階段,讓GC線程、用戶線程交替執行,盡量減少GC線程獨占CPU,這樣垃圾收集過程更長,但是對用戶程序影響小一些。實際上i-CMS效果很一般,目前已經被聲明為“deprecated”。
CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure“,失敗后而導致另一次Full GC的產生。由于CMS并發清理階段用戶線程還在運行,伴隨程序的運行自熱會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之后,CMS無法在本次收集中處理它們,只好留待下一次GC時將其清理掉。這一部分垃圾稱為“浮動垃圾”。也是由于在垃圾收集階段用戶線程還需要運行,即需要預留足夠的內存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分內存空間提供并發收集時的程序運作使用。在默認設置下,CMS收集器在老年代使用了68%的空間時就會被激活,也可以通過參數-XX:CMSInitiatingOccupancyFraction的值來提高觸發百分比,以降低內存回收次數提高性能。JDK1.6中,CMS收集器的啟動閾值已經提升到92%。要是CMS運行期間預留的內存無法滿足程序其他線程需要,就會出現“Concurrent Mode Failure”失敗,這時候虛擬機將啟動后備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所以說參數-XX:CMSInitiatingOccupancyFraction設置的過高將會很容易導致“Concurrent Mode Failure”失敗,性能反而降低。
最后一個缺點,CMS是基于“標記-清除”算法實現的收集器,使用“標記-清除”算法收集后,會產生大量碎片??臻g碎片太多時,將會給對象分配帶來很多麻煩。比如說大對象,內存空間找不到連續的空間來分配不得不提前觸發一次Full GC。為了解決這個問題,CMS收集器提供了一個-XX:UseCMSCompactAtFullCollection開關參數,用于在Full GC之后增加一個內存碎片的合并整理過程,但是內存整理過程是無法并發的,因此解決了空間碎片問題,卻使停頓時間變長。還可通過-XX:CMSFullGCBeforeCompaction參數設置執行多少次不壓縮的Full GC之后,跟著來一次碎片整理過程(默認值是0,表示每次進入Full GC時都進行碎片整理)。
G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。
具體實現思路在G1之前的其他收集器進行收集的范圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的內存布局與就與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。G1跟蹤各個Region里面的垃圾堆積的價值大?。ɑ厥账@得的空間大小以及回收所需時間的經驗值),在后臺維護一個優先列表,每次根據允許的收集時間,優先回價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內獲可以獲取盡可能高的收集效率。
但是,G1把內存“化整為零”的思路,理解起來似乎很容易理解,其中的實現細節卻遠遠沒有現象中簡單,否則也不會從04年Sun實驗室發表第一篇G1的論文拖至今將近8年時間都還沒有開發出G1的商用版。筆者舉個一個細節為例:把Java堆分為多個Region后,垃圾收集是否就真的能以Region為單位進行了?聽起來順理成章,再仔細想想就很容易發現問題所在:Region不可能是孤立的。一個對象分配在某個Region中,它并非只能被本Region中的其他對象引用,而是可以與整個Java堆任意的對象發生引用關系。那在做可達性判定確定對象是否存活的時候,豈不是還得掃描整個Java堆才能保障準確性?這個問題其實并非在G1中才有,只是在G1中更加突出了而已。在以前的分代收集中,新生代的規模一般都比老年代要小許多,新生代的收集也比老年代要頻繁許多,那回收新生代中的對象也面臨過相同的問題,如果回收新生代時也不得不同時掃描老年代的話,Minor GC的效率可能下降不少。。
在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) ②.并發標記(Concurrenr Marking) ③.最終標記(Final Marking) ④.篩選回收(Live Data Counting And Evacution)
1.初始標記:初始標記僅僅標記GC Roots能直接關聯到的對象,并且修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序并發運行時,能在正確可用的Region中創建新的對象。這階段需要停頓線程,不可并行執行,但是時間很短。
2.并發標記:此階段是從GC Roots開始對堆中對象進行可達性分析,找出存活對象,此階段時間較長可與用戶程序并發執行。
3.最終標記:此階段是為了修正在并發標記期間因為用戶線程繼續運行而導致標記產生變動的那一份標記記錄,虛擬機將這段時間對象變化記錄在線程Remembered Set Logs里面,最終標記階段需要把Remembered Set Logs的數據合并到Remembered Set中,這段時間需要停頓線程,但是可并行執行。
4.篩選回收:對各個Region的回收價值和成本進行排序,根據用戶期望的GC停頓時間來制定回收計劃。
-XX:+
Client、Server模式默認GC Sun/Oracle JDK GC組合方式 總結表面上看,Java 和 C 比起來,由于內存的動態分配與內存回收技術已經相對成熟,日常的代碼中也不怎么需要關注內存的申請與釋放。為什么我們還要關注這些問題呢?
筆者認為,一方面越是平常不會關注的東西,在關鍵的時候越珍貴,因為存在排查各種內存溢出、內存泄漏問題、又或者當垃圾收集稱為系統達到更高并發量瓶頸時,對這些“自動化”功能細節的了解,為我們提供了更廣闊的思路。另一方面,不同業務場景總有相似的一面,今天借鑒到的實現思想的細節,一直積累下去,或許未來的某天突然就豁然開朗了。
參考文章《HotSpot垃圾收集器》——stack_over_flow@博客園
《解析JDK 7的Garbage-First收集器》——周志明@InfoQ
《深入理解Java虛擬機:JVM高級特效與最佳實現》,第2-3章——周志明著
Java系列筆記 - Java 內存區域和GC機制——明舞
深入理解JVM結構——java團長
聯系作者zhihu.com
segmentfault.com
oschina.net
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/70114.html
摘要:運行時數據區域的學習,是學習以及機制的基礎,也是深入理解對象創建及運行過程的前提。了解內存區域劃分,是學習概念的前提。 Java 運行時數據區域的學習,是學習 jvm 以及 GC 機制的基礎,也是深入理解 java 對象創建及運行過程的前提。廢話不多說,直接進入正題: 一張圖總結 showImg(https://segmentfault.com/img/bVOMAn?w=685&h=5...
摘要:看來還是功力不夠,索性拆成了六篇文章,分別從自動內存管理機制類文件結構類加載機制字節碼執行引擎程序編譯與代碼優化高效并發六個方面來做更加細致的介紹。本文先說說虛擬機的自動內存管理機制。在類加載檢查通過后,虛擬機將為新生對象分配內存。 歡迎關注微信公眾號:BaronTalk,獲取更多精彩好文! 書籍真的是常讀常新,古人說「書讀百遍其義自見」還是蠻有道理的。周志明老師的這本《深入理解 Ja...
摘要:抽時間重新讀了一遍深入理解一書。驗證確保文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全??梢娦钥梢娦允侵府斠粋€線程修改了共享變量的值,其他線程能夠立即得知這個修改。 抽時間重新讀了一遍《深入理解JVM》一書。以下為摘錄內容。 1 java內存區域 showImg(https://segmentfault.com/img/bVboDgk?w=617&h=365...
摘要:目錄往期博客課堂篇初識常量池簡單理解字符串常量池靜態常量池大整型常量池為什么要了解垃圾收集和內存分配如何判斷對象已死引用計數算法可達性分析算法之后引用的擴充回收方法區垃圾收集算法分代收集理論標記清除標記復制標記整理對象分 ...
摘要:堆和方法區只有在程序運行時才能確定內存的使用情況,垃圾回收器所關注的主要就是這部分內存。虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整比率參數以提供最合適的停頓時間或最大的吞吐量。 Tip:內容為對《深入理解Java虛擬機》(周志明 著)第三章內容的總結和筆記。這是第一次拜讀時讀到的一些重點,做個分享,也為后面再次閱讀和實踐做保障。 3.1 概述 程序計數器、虛擬機棧、本地...
閱讀 2737·2021-11-11 17:21
閱讀 628·2021-09-23 11:22
閱讀 3591·2019-08-30 15:55
閱讀 1652·2019-08-29 17:15
閱讀 585·2019-08-29 16:38
閱讀 922·2019-08-26 11:54
閱讀 2520·2019-08-26 11:53
閱讀 2768·2019-08-26 10:31