深入理解JVM(四):垃圾收集

垃圾收集

棧中的棧幀隨着方法的進入和退出而有條不紊的執行這出棧和入棧操作。

每一個棧幀中分配多少內存基本上是在類結構確定下來是就已知的,因此這幾個區域的內存分配和回收都具備確定性,在這幾個區域內不需要過多考慮回收問題,因爲方法結束或者線程結束時,內存就跟着回收了。

因此我們主要學習java堆和方法區的內存分配和回收。

1.判斷對象存活

1.1 引用計數器法

給對象添加一個引用計數器,每當由一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器爲0的對象就是不可能再被使用的

1.2 可達性分析算法

通過一系列的成爲“GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑成爲引用鏈,當一個對象到GC ROOTS沒有任何引用鏈相連時,則證明此對象時不可用的
Java語言中GC Roots的對象包括下面幾種:

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

可達性分析算法判定對象是否可回收

2.引用

強引用就是在程序代碼之中普遍存在的,類似Object obj = new Object() 這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象
軟引用用來描述一些還有用但並非必須的元素。對於它在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收,如果這次回收還沒有足夠的內存纔會拋出內存溢出異常
弱引用用來描述非必須對象的,但是它的強度比軟引用更弱一些,被引用關聯的對象只能生存到下一次垃圾收集發生之前,當垃圾收集器工作時,無論當前內存是否足夠都會回收掉只被弱引用關聯的對象
虛引用的唯一目的就是能在這個對象被收集器回收時收到一個系統通知

3.Finalize方法

任何一個對象的finalize()方法都只會被系統自動調用一次,如果對象面臨下一次回收,它的finalize()方法不會被再次執行,因此第二段代碼的自救行動失敗了

3.1 回收方法區

永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類
廢棄常量:假如一個字符串abc已經進入了常量池中,如果當前系統沒有任何一個String對象abc,也就是沒有任何Stirng對象引用常量池的abc常量,也沒有其他地方引用的這個字面量,這個時候發生內存回收這個常量就會被清理出常量池
無用的類:

1.該類所有的實例都已經被回收,就是Java堆中不存在該類的任何實例
2.加載該類的ClassLoader已經被回收
3.該類對應的java.lang.Class對象沒有在任何地方被引用,
  無法再任何地方通過反射訪問該類的方法

4.垃圾收集算法

4.1 標記—清除算法

算法分爲標記和清除兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象、
不足:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清楚之後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後再程序運行過程中需要分配較大的對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作
標記清除算法

4.2 複製算法

他將可用內存按照容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可
不足:將內存縮小爲了原來的一半
實際中我們並不需要按照1:1比例來劃分內存空間,而是將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor
當另一個Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔保機制進入老年代
複製算法

4.3 標記整理算法

讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存
標記整理算法

4.4 分代收集算法

只是根據對象存活週期的不同將內存劃分爲幾塊。一般是把java堆分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記清理或者標記整理算法來進行回收

5.垃圾收集器

Java內存
HotSpot虛擬機的垃圾收集器

a)Serial收集器:

這個收集器是一個單線程的收集器,但它的單線程的意義不僅僅說明它會只使用一個COU或一條收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。
Serial/Serial Old收集器運行示意圖

b)ParNew 收集器:

Serial收集器的多線程版本,除了使用了多線程進行收集之外,其餘行爲和Serial收集器一樣
並行:指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態
併發:指用戶線程與垃圾收集線程同時執行(不一定是並行的,可能會交替執行),用戶程序在繼續執行,而垃圾收集程序運行於另一個CPU上。
ParNew 與Serial Old收集器運行示意圖

c)Parallel Scavenge

收集器是一個新生代收集器,它是使用複製算法的收集器,又是並行的多線程收集器。
吞吐量:就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值。即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)。
新生代Parallel Scavenge和年老代Parallel Old收集器搭配運行過程圖

d)Serial Old 收集器:

是Serial收集器的老年代版本,是一個單線程收集器,使用標記整理算法
e)Parallel Old 收集器:
Parallel Old是Paraller Seavenge收集器的老年代版本,使用多線程和標記整理算法

f)CMS收集器:

CMS收集器是基於標記清除算法實現的,整個過程分爲4個步驟:

  • 1.初始標記2.併發標記3.重新標記4.併發清除

  • 優點:併發收集、低停頓

  • 缺點:

  • 1.CMS收集器對CPU資源非常敏感,CMS默認啓動的回收線程數
    是(CPU數量+3)/4,

  • 2.CMS收集器無法處理浮動垃圾,可能出現Failure失敗
    而導致一次Full G場地產生
  • 3.CMS是基於標記清除算法實現的
    CMS收集器工作過程

g)G1收集器:

它是一款面向服務器應用的垃圾收集器

  • 1.並行與併發:利用多CPU縮短STOP-The-World停頓的時間
    2.分代收集
    3.空間整合:不會產生內存碎片
    4.可預測的停頓

運作方式:

  • 初始標記,併發標記,最終標記,篩選回收

6.內存分配與回收策略

6.1 對象優先在Eden分配:

大多數情況對象在新生代Eden區分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC

6.2 大對象直接進入老年代:

所謂大對象就是指需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組。這樣做的目的是避免Eden區及兩個Servivor之間發生大量的內存複製

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

如果對象在Eden區出生並且盡力過一次Minor GC後仍然存活,並且能夠被Servivor容納,將被移動到Servivor空間中,並且把對象年齡設置成爲1.對象在Servivor區中每熬過一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(默認15歲),就將會被晉級到老年代中

6.4動態對象年齡判定

爲了更好地適應不同程序的內存狀況,虛擬機並不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉級到老年代,如果在Servivor空間中相同年齡所有對象的大小總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入到老年代,無須登到MaxTenuringThreshold中要求的年齡

6.4 空間分配擔保:

在發生Minor GC 之前,虛擬機會檢查老年代最大可 用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那麼Minor DC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許那麼會繼續檢查老年代最大可用的連續空間是否大於晉級到老年代對象的平均大小,如果大於,將嘗試進行一次Minor GC,儘管這次MinorGC 是有風險的:如果小於,或者HandlePromotionFailure設置不允許冒險,那這時也要改爲進行一次Full GC

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