深入理解Java虛擬機-垃圾收集器及內存分配策略

  • 哪些內存需要回收
  • 什么時候回收
  • 如何回收

如何判斷對象是否存活

可達性分析算法(Reachability Analysis): 通過一系列"GC Roots"對象作為起始點,從這些節點開始往下搜索,搜索過的路徑稱為引用鏈(Reference Chain),當任意一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象不可用。

GC Roots對象包括以下幾種:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象
  • 方法區中類靜態變量屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中JNI(本地Native方法)引用的對象

引用包括:

  • 強引用:Java代碼中類似Object obj = new Object()這類的引用。只要有強引用在,垃圾收集器永遠不會回收掉被引用的對象
  • 軟引用:用來描述一些還有用但并非必要的對象。對于軟引用關聯著的對象,在系統發生內存溢出之前,將會把這些對象列進回收范圍之中進行二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。
  • 弱引用:比軟引用更弱一些,只能存活到垃圾收集活動發生之前。
  • 虛引用:它是最弱的一種引用關系,它存在的目的就是能在這個對象被收集器回收時收到系統通知。

對象的死亡

在可達性分析算法中不可達的對象,至少要經歷兩次標記的過程,才確定是否被回收:

1、如果對象在可達性分析后發現沒有與GC Roots相連的引用鏈,那它將會被第一次標記并進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法時,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為“沒有必要執行”。

2、如果對象有必要執行finalize()方法,那么這個對象將會被放在一個F—Queue隊列之中,虛擬機的finalizer線程會執行它

3、finalize()方法是對象逃脫死亡的最后一次機會,稍后GC將對F-Queue隊列中的對象進行第二次小規模的標記,如果對象成功在finalize()中拯救自己(建立與引用鏈的聯系),那在第二次標記時它將被移除出“即將回收”的集合;否則它就要被回收。

方法區的回收(永久代)

方法區垃圾收集的效率是比較低的,收集的內容主要有兩部分:廢棄的常量和無用的類

無用的類需要符合三個條件:

  • 該類的所有實例都已經被回收
  • 加載該類的ClassLoader已經被回收
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法

垃圾收集算法

標記-清除算法

標記-清除算法分兩階段:首先標記所有需要回收的對象,在標記完成后統一回收所有被標記的對象

標記-清除算法的不足:第一兩個過程的效率都不高;第二會產生內存碎片

復制算法

復制算法是把內存分成相等的兩塊,每次只使用其中的一塊。當這一塊內存用完了,就將還存活的對象復制到另外一塊內存上,然后將當前這塊內存一次清理掉。

復制算法的好處就是實現簡單,效率高,不足之處就是浪費內存空間。

如果對象的存活率較高,復制操作的效率會比較低,所以復制算法適合對象存活率較低的情況。

現在商業虛擬機及HotSpot虛擬機都采用這種算法來回收新生代。新生代的內存空間會分為Eden、From Survivor和To Survivor三塊,每次使用Eden空間和From Survivor空間。當進行回收時,將Eden空間及From Survivor空間存活的對象復制到To Survivor,然后清理Eden和From Survivor空間。

標記-整理算法

標記-整理算法分兩階段:首先標記需要回收的對象,然后讓所有存活對象往一端移動,然后直接清理端邊界以外的內存。

標記-整理算法適合對象存活率較高的情況,并且不會產生內存碎片

分代收集算法(Generational Collection)

一般把Java堆分成新生代和老年代:

  • 新生代的對象存活率較低,使用復制算法

  • 老年代的對象存活率較高,使用“標記-清除”或“標記-整理”算法

HotSpot的算法實現

枚舉根節點

必須確保可達性分析執行的效率:HotSpot虛擬機中使用一組OopMap(Ordinary Object Pointer)普通對象指針存放對象的引用。在類加載完成的時候,HotSpot就把對象內什么偏移量是什么類型的數據計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。

必須確??蛇_性分析的執行時在“一致性”的快照中進行:在整個分析期間整個執行系統看起來像被凍結在某個時間點上(Stop The World)。

安全點(Safepoint)

安全點是HotSpot生成OopMap的位置,也是程序停下來GC的地方。安全點的選定基本是以程序“是否具有讓程序長時間執行的特征”為標準進行選定,“長時間執行”的最明顯特征就是指令序列復用,例如方法的調用、循環跳轉、異常跳轉等。

如果在GC時讓所有的線程跑到安全點再停頓:

  • 搶先式中斷:首先讓所有線程中斷,如果發現有線程中斷的地方不在安全點上,就恢復線程,讓它“跑”到安全點。目前基本沒有虛擬機才用這種方式
  • 主動式中斷:當GC需要中斷線程時,不直接對線程操作,僅僅簡單地設置一個標志,各個線程執行時主動輪詢這個標志,發現中斷標識為真時就自己中斷掛起。輪詢標志的地方和安全點是重合的,另外再加上創建對象需要分配內存的地方。

安全區域(Safe Region)

安全區域是安全點的擴展,解決了程序在“不執行”時候的GC安全問題:如果程序處在Sleep或Blocked的狀態,這個時候線程無法響應JVM的中斷請求,無法走到安全點。

在線程執行到Safe Region中的代碼時,首先標識自己已經進入了Safe Region。這樣,當這段時間內發生GC時,就不用管標識自己為Safe Region狀態的線程了。在線程要離開Safe Region時,它要檢查系統是否已經完成根節點枚舉(整個GC過程),如果完成,線程就繼續執行將,否則它就必須等待直到收到可以安全離開Safe Region的信號為止。

垃圾收集器

  • 并行(Parallel):指多條垃圾收集線程并行工作,但此時用戶線程仍然處于等待情況
  • 并發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定并行,可能會交替執行),用戶程序繼續,而垃圾收集程序運行于另外一個CPU上

Serail、ParNew、Parallel Scavenge : 復制算法

CMS:標記-清除算法

Parallel Old、Serial Old、G1:標記-整理算法

Serial 收集器

Seral 是一個單線程的收集器。它只會使用一個CPU或者一條收集線程去完成垃圾收集的工作,在它進行垃圾收集時,必須暫停其他所有的工作線程。

多CPU的情況下,效率比較低。但對于單CPU的情況下,因為是單線程,減少了線程之間的交互,專注垃圾收集的工作,效率會比較高,適用于Client模式下的虛擬機。

ParNew 收集器

ParNew是Serial的多線程版。除了使用多條線程進行垃圾收集之外,其余行為都和Serial一樣。它默認開啟的收集線程和CPU的數量相同,在CPU非常多的情況下,可以通過參數-XX:ParallelGCThreads限制垃圾收集的線程數。ParNew是目前唯一能和CMS配合工作的收集器。

在單CPU的情況下,因為存在線程交互的開銷,ParNew的性能不一定比Serial收集器的好。不過在當前CPU動輒4核的情況下,它是許多運行在Server模式下的虛擬機中首選的新生代收集器。

使用-XX:UserConcMarkSweepGC或者-XX:UserParNewGC選項來指定

Parallel Scavenge 收集器

Parallel Scavenge 是一個并行的多線程收集器,它的目標是達到一個可控的吞吐量(Throughput),因此也被稱為“吞吐量優先”收集器。

吞吐量就是CPU用于運行用戶代碼的時間與CPU總消耗時間的比值,吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)。

停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能提升用戶的體驗,而高吞吐量則可以高效地利用CPU時間,盡快地完成程序的運算任務,主要適合后臺運算而不需要太多交互的任務。

Parallel Scavenge收集器提供兩個參數用于精確控制吞吐量:

  • -XX:MaxGCPauseMillis:最大的GC停頓時間。允許一個大于0的毫秒數,收集器盡可能保證內存回收花費的時間不超過設定值。但這并不意味這個值越小越好,因為GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的。因為收集300M的新生代肯定比收集500M的新生代快,原來10秒收集一次,每次停頓100毫秒,現在5秒收集一次,每次停頓70毫米,停頓時間是下降了,但吞吐量也降低了。
  • -XX:GCTimeRatio:垃圾收集時間戰總時間的比率,相當于是吞吐量的倒數。如果把此參數設置為19,那允許的最大GC時間就占5%(1/(1+19)),默認值是99,就是允許最大1%(1/(1+99))的垃圾收集時間。

GC自適應調節策略(GC Ergonomics):-XX:UseAdaptiveSizePolicy 當這個參數打開,就不需要手工指定新生代的大?。?Xmn)、Eden與Survivor的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數,虛擬機會根據當前系統的運行情況收集性能監控,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。

Serial Old 收集器

Serial Old 是Serail收集器的老年代版本,適合于給Client模式下的虛擬機使用。如果在Server模式下:一種用途是在JDK1.5及之前的版本和Parallel Scavenge搭配使用;另一種用途就是作為CMS收集器的后備預案,在并發收集發生Concurrent Mode Failure時使用

Parallel Old 收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法,在JDK1.6中才開始提供。

CMS(Concurrent Mark Sweep) 收集器

CMS 收集器是一種以獲取最短回收停頓時間為目標的收集器。目前大部分應用在互聯網站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統的停頓時間最短,以給用戶帶來較好的體驗。

CMS收集器基于“標記-清除”算法實現,整個過程分為4步驟:

  • 初始標記(CMS inital mark) 需要“Stop The World”,僅僅就只是標記一下GC Roots能直接關聯到的對象
  • 并發標記(CMS Concurrent mark) GC Roots枚舉階段
  • 重新標記(CMS remark) 需要“Stop The World”,該階段是修正并發標記期間因為用戶線程繼續運作導致標記產生變動的那部分對象的標記記錄
  • 并發清除(CMS concurrent sweep)

CMS收集器提供了并發收集、低停頓等優點,但也有很明顯的缺點:

1、CMS收集器對CPU資源敏感:CMS默認啟動的回收線程數是(CPU數量+3)/4,也就是當CPU在4個以上時,并發回收時垃圾收集線程占不少于25%的CPU資源,并且隨著CPU數量的增加而降低。但如果CPU少于4個時(比如2個時),CMS對用戶程序的影響就會比較大。

2、CMS收集器無法處理浮動(Float Garbage)垃圾,可能出現“Concurrent Mode Failure”失敗導致發生另外一次Full GC。

  • CMS在并發清理階段用戶線性還在運行,伴隨用戶線程的運行自然就會有新的垃圾不斷產生,CMS無法在當次處理這些垃圾,只能在下次GC時處理,這些垃圾稱為“浮動垃圾”。

  • 由于用戶線程在GC的時候還在運行,所以必須預留足夠的內存空間給用戶線程,當預留的內存空間無法滿足用戶線程時,就會出現“Concurrent Mode Failure”失敗,CMS啟動后備方案:臨時啟動Serial Old進行老年代的垃圾收集,此時,停頓時間就會比較長。CMS收集器通過參數-XX:CMSInitiatingOccupancyFraction 來激活自己,JDK1.5默認設置當老年代使用68%觸發CMS收集器,JDK1.6中,此值為92%。

3、CMS收集器采用“標記-清除”算法,會產生大量的空間碎片。會導致老年代還有很多空間,因為找不到足夠大的連續空間不得不進行Full GC。

  • -XX:UseCMSCompactAtFullCollection:開關參數,默認是開啟,用于在CMS收集器頂不住需要進行Full GC是開啟內存碎片整理,這樣會導致停頓時間變長
  • -XX:CMSFullGCsBeforeCompaction:這個參數用于設置執行多少次不壓縮的Full GC后,跟著來一次帶壓縮的。默認為0,表示每次進入Full GC時都進行內存碎片整理

G1 收集器

G1是一款面向服務端應用的垃圾收集器,它有以下幾個特點:

  • 并行與并發:G1重復利用硬件資源優勢,使用多個CPU核心來縮短Stop-The-World停頓時間
  • 分代收集:分代的概念依然在G1中保留
  • 空間整合:G1從整體上看采用了“標記-整理”的算法,局部(連個Region)來看是基于復制算法,這樣保證了G1運行期間不會產生內存碎片
  • 可預測停頓:G1除了最求低停頓外,還能建立可預測的停頓模型。能讓使用明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不超過N毫秒。

G1收集器Java堆的布局:將整Java堆劃分多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,他們都是一部分Region(不需要連續)的集合。

G1創建可預測的停頓時間模型:G1跟蹤各個Region里面的垃圾堆積的價值大?。ɑ厥账@得的空間大小及回收所需要時間的經驗值),在后臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region,這也是Garbage-First名稱的來由。

G1如何避免全堆掃描:使用Remembered Set來避免全堆掃描。G1中的每個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對Reference類型的數據進行寫操作時,會產生一個Write Barrier暫停中斷寫操作,檢查Reference引用的對象是否處于不同的Region之中(在分代的例子中就是是否有老年代的對象引用了新生代的對象)。如果是,便通過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set之中。當進行內存回收時,在GC根節點的枚舉范圍假如Remembered Set即可。

G1收集器的回收步驟可分為以下步驟:

  • 初始標記(Initial Marking):僅僅標記GC Roots能直接關聯到的對象,并且修改TAMS(Next Top at Mark Start)的值,讓下一個階段用戶程序并發運行時,能在正確的Region中創建新對象,這個階段需要停頓用戶線程。
  • 并發標記(Concurrent Marking):從堆的GC RootS開始對堆的對象進行可達性分析,找出存活的對象,這個階段用戶線程可并發執行
  • 最終標記(Final Marking):修正由于并發階段用戶線程執行運行導致標志產生變動的那一部分標記記錄。虛擬機將這段時間對象變化記錄在線程Remembered Set Logs里面,最終標記階段需要把Remembered Set Logs的數據合并到Remembered Set中。這階段需要停頓用戶線程,當標記線程可以并行執行。
  • 篩選回收(Live Data Counting and Evacuating ):首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來指定回收計劃。

內存的分配與回收策略

對象優先在Eden分配

  • 新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因為Java對象大多數具備朝生夕滅的特征,所以Minor GC非常頻繁,一般回收速度也很快。
  • 老年代(Major GC/Full GC): 指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC(Parallel Scavenge收集器例外),Major GC 一般比Minor GC慢10倍以上。

大對象直接進入老年代

大對象:需要大量連續空間的Java對象,比如:長的字符串和數組。對大對象,我們可以通過參數設置,讓它直接在老年代分配,從而避免頻繁的Minor GC。

-XX:PretenureSizeThreshold:可以讓對象直接在老年代分配

長期存活的對象進入老年代

對象年齡技術器:虛擬機給每個對象定義了一個對象年齡計數器。如果對象在Eden出生并經過第一次Minor GC后仍然存活,并且能被Survivor容納的話,將被移動到Survivor空間,并且對象的年齡設定為1。對象在Survivor區每熬過一次Minor GC,年齡就增加1。當它的年齡增加到一定程度(默認為15),就將會晉升到老年代。對象晉升到老年代的閥值,可以通過參數-XX:MaxTenuringThreshold設置。

動態對象年齡判斷

虛擬機并不是永遠要求對象的年齡必須達到MaxTenuringThreshold才能晉升老年代。如果在Survivor空間中相同年齡的所有對象大小的總和大于Survivor空間的一半,年齡大于或者等于該年齡的對象就可以直接進入老年代。

空間分配擔保

在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果條件成立,那么Minor GC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那么繼續檢查老年代最大可用連續空間是否大于歷次晉升老年代對象的平均大小,如果大于,將嘗試進行一次Minor GC;如果小于,或者HandlePromotionFailure設置不允許,需要進行一次Full GC。

但是,在JDK1.6 Update24之后,規則變為只要老年代的連續空間大于新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,否則將進行Full GC。

理解GC日志

打印GC日志:-XX:PrintGCDetails

GC日志 待補充……

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容