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