JVM(二):垃圾收集器,內存分配策略

確定對象是否存活

在堆裏存放着Java中幾乎所有的對象實例,垃圾收集器在對堆進行回收之前,需要確定哪些對象還“存活”,哪些對象已經“死亡”。常用的算法有兩種:引用計數法可達性分析算法兩種

引用計數法

給對象添加一個引用計數器,當對象被引用時,計數器+1,當引用失效時,計數器-1;當任何時刻計數器爲0時,說明對象不可能再被使用。

引用計數法的實現簡單,效率也高,在大部分場景都是一個不錯的算法。但有一個致命的問題:無法解決對象之間循環引用的問題。現在的虛擬機基本都不是使用引用計數法來判斷對象是否存活。

可達性分析算法

以一系列叫做"GC Roots"的對象作爲起始點,向下進行搜索,搜索經過的路徑叫做引用鏈。當一個對象到GC Roots之間沒有任何引用鏈相連,說明此對象是不會再被使用的。優點類似於圖論算法。

可作爲GC Roots的對象一般包括以下幾種:

  • 虛擬機棧中引用的對象
  • 本地方法棧中引用的對象
  • 方法區中常量引用的對象
  • 方法區中靜態變量引用的對象

引用

強引用

強引用是在程序代碼中普遍存在的,類似於Object a=new Object()這樣的就是強引用。只要強引用還存在,那麼與之關聯的對象就永遠不會被垃圾回收器回收

軟引用

軟引用描述的是一些還有用但是非必需的對象。與軟引用關聯的對象一般情況下不會被回收,只有當系統即將發生內存溢出異常時,纔會將這些對象劃入回收範圍進行二次回收,若二次回收之後依然沒有足夠的內存,此時就會拋出內存溢出異常

弱引用

弱引用描述的同樣時有用但是非必需的對象,但是它的強度比軟引用更弱一點。被弱引用關聯的對象只能生存到下一個垃圾收集發生之前。也就是說,當垃圾收集器工作時,所有隻與弱引用關聯的對象都會被回收

虛引用

虛引用是強度最弱的一種引用關係。虛引用的存在與否對對象是否存活不構成任何影響。爲對象設置虛引用的唯一目的就是在這個對象被垃圾收集器回收時得到一個系統通知。

回收方法區

方法區中也是有垃圾產生的,只是回收方法區的性價比比較低,所以虛擬機規範中並沒有要求一定要在方法區實現垃圾收集。而方法區的垃圾收集主要分爲兩部分:廢棄常量無用的類

廢棄常量

回收廢棄常量與回收堆中的實例非常相似。以字符串來例,假設一個字符串“abc”已經進入了方法區常量池,但此時程序代碼中沒有任何一個引用指向這個字符串,那麼這個字符串就會被視爲無用的,可以被清理。

無用的類

一個類滿足以下三種條件就可以被認爲是無用的:

  • 該類的所有實例都已經回收,Java堆中不存在該類的實例
  • 該類相關的ClassLoader已經被回收
  • 該類的Class對象沒有在任何地方被引用,也就是說無法通過反射訪問該類

垃圾收集算法

標記清除法(mark-swap)

首先標記出所有需要回收的對象,在標記完成後統一進行清除。

缺點:

1.效率問題:標記和清除這兩個過程的效率都不高

2.有內存碎片產生:標記清除後會產生大量不連續的內存碎片,如果後續需要一個大對象,可能會因內存不足提前引起一次垃圾收集

複製算法(copy)

將內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊,當一塊內存使用完了,將還存活的對象複製到另一塊上,然後把已經使用過的內存塊全部清理掉

優點:沒有內存碎片的問題

缺點:每次只能使用50%的內存,內存利用率不高

所以現在的收集器如果使用複製算法的話,基本都不會使用這種劃分爲大小相等的兩塊這樣的做法。由於大多數對象的存活時間都很短,所以說一種典型的做法就是將內存劃分爲一塊較大的Eden區和兩塊較小的survivor區。每次只使用Eden區和其中的一塊Survivor區,當發生垃圾回收的時候,將存活的對象一次性複製到另一個survivor區中,然後對原先的兩個區域進行清理。若複製到survivor區時,survivor的空間不足以存放所有的存活對象,此時就出發老年代的分配擔保

標記整理法(mark-compact)

同樣是對所有需要回收的對象進行標記,在標記完成後,將所有存活的對象朝一端移動,然後清除掉邊界以外的所有對象。

優點:解決了內存碎片的問題

缺點:涉及對象在物理地址上的移動,效率較低

分代收集算法

根據對象存活年齡將內存劃分爲幾塊,常見的分法是分成新生代和老年代。因爲新生代中的對象存活率較低,所有可以次採用複製算法;而老年代中的對象存活率高,且沒有額外的空間來做分配擔保,所以可以採用標記清理或者標記整理法

GC觸發的時機

安全點

程序並不是在任何地方都能停頓下來進行GC,只有在到達安全的地方纔能暫停,這些位置就叫做**安全點(safe point)**安。安全點的選定不能太少,太少容易導致GC等待時間太長,垃圾堆積佔用內存;也不能設置得太短,頻繁的GC也會增加開銷。那麼安全點一般在方法調用,循環跳轉,異常跳轉等指令上產生,因爲這些指令更可能會長時間執行。

另一個需要考慮的問題就是如何確保GC發生時,所有線程都在安全點上?這裏有兩種方法:搶先式中斷主動式中斷

搶先式中斷

當要發生GC時,直接將所有線程中斷,如果有線程中斷的地方不在安全點上,那麼就恢復線程,等它跑到安全點上。現在幾乎沒有虛擬機採用這種方式

主動式中斷

當要發生GC時,不直接操作線程,而是修改一箇中斷標誌,當線程執行到安全點時,都會去輪詢這個標誌,如果該標誌被修改爲true,那麼中斷掛起。這樣能確保每個線程都能在安全點發生GC

安全區域

因爲中斷標誌需要主動輪詢,所以對於那些正處於沉睡狀態或者是中斷狀態的線程來說是不可實現的,並且虛擬機也不可能等到這些線程被重新調度然後再跑到安全點。

安全區域指的就是不會使引用關係發生變化的代碼片段。當要發生GC的時候,如果線程處於安全區域,那麼虛擬機就不會管這些線程了,因爲它的執行不會導致引用關係的變化。但是線程離開安全區域時,還需要檢查標記過程是否已經結束,若還沒結束,則必須等待,直到收到離開的信號才能離開安全區域

垃圾收集器

現在一般都採用分代收集的算法,並分爲新生代和老年代。所以其中的垃圾收集器也有所不同

新生代

Serial

單線程的垃圾收集器,在執行垃圾收集的時候,必須暫停所有工作線程(Stop the world)。採用複製算法

優點:實現簡單,單線程,沒有線程交互的開銷。

缺點:必須暫停所有工作線程,有卡頓產生。

客戶端應用適合使用Serial收集器,因爲客戶端應用一般較小,而且對象數量也比較少,垃圾收集耗費的時間也比較短,即使Serial會暫停工作線程,但這個卡頓對用戶來說不太明顯。所以客戶端應用適合使用Serial

ParNew

其實就是Serial的多線程版本,同樣會暫停工作線程,並且也採用複製算法。

可以和CMS一起配合工作(CMS無法和Parallel Scavenge配合使用)

優點:在多CPU環境下充分利用硬件優勢

缺點:如果是在單CPU或者雙CPU的環境下,性能可能比不上Serial,因爲有線程交互的開銷

Parallel Scavenge

同樣也是一個多線程收集器,也是使用複製算法。但和前兩種區別就是,Serial和ParNew關注停頓時間,而Parallel Scavenge關注的是吞吐量,吞吐量就是CPU運行用戶代碼時間與總時間的比值。

由於關注吞吐量,所以也有幾個參數可以進行準確控制:

-XX:MaxGCPauseMillis 控制最大垃圾收集停頓時間。但不是越小越好,因爲時間的縮短是要以新生代空間的減小爲代價的。因爲更小的空間,進行GC的時間也越短

-XX:GCTimeRatio 直接控制吞吐量。GCTimeRatio參數的值應當是一個大於0且小於100的整數,也就是垃圾收集時間佔總時間的比率,相當於是吞吐量的倒數。如果把此參數設置爲19,那允許的最大GC時間就佔總時間的5%(即1/(1+19)),默認值爲99,就是允許最大1%(即1/(1+99))的垃圾收集時間。

還有一個很便捷的開關:

-XX:+UseAdaptiveSizePolicy 打開之後,不需要手動設置新生代大小,Eden區和Survivor區比例,晉升老年代對象年齡等參數,虛擬機會進行動態地調整

老年代

Serial Old

Serial Old是Serial的老年代版本,同樣是一個單線程收集器,但採用的是標記i整理算法。

主要用途:

1.Client模式下,是一個好的選擇

2.Server模式下,可以和Parallel Scavenge搭配使用

3.作爲CMS收集器的後備預案

Parallel Old

是Parallel Scavenge的老年代版本,採用多線程和標記整理算法。這個收集器是在JDK1.6開始才提供的。在此之前,Parallel Scavenge只能和Serial Old搭配使用。而應用於服務端的話,Serial Old的單線程特點會拖累性能,所以未必能發揮出Parallel Scavenge的吞吐量優先這一性能。所以Parallel Old的出現,就能和Parallel Scavenge進行搭配,成爲了真正意義上的“吞吐量優先”收集器。

CMS收集器

CMS(Cocurrent Mark Swap)是一種以獲取最短停頓時間爲目標的收集器,它使用標記整理算法。因爲CMS的停頓時間很短,所以廣泛應用於重視響應速度的服務中。

CMS收集器的運行過程主要包括四個步驟:

初始標記

初始標記僅僅標記那些與GC Roots直接關聯的對象,雖然說這個過程會暫停所有工作線程,但是由於執行速度很快,所以停頓時間也很短

併發標記

這個是執行時間比較長的一個操作,它在初始標記的基礎上標記出所有能和GC Roots對象關聯到的對象。最大的特點就是這個操作無需暫停工作線程,所以就叫做併發標記。

重新標記

併發標記的時候,因爲工作線程一直在執行,所以對象的引用關係很可能發生變化,重新標記是爲了修正併發標記期間由於用戶程序執行而導致的引用關係變化的記錄。這個操作需要暫停工作線程。

併發清除

真正執行GC,這個操作無需暫停工作線程

CMS的缺點:

1.對CPU資源敏感。因爲CMS最大的特點是某些操作可以和工作線程併發執行,所以它會佔用一部分線程,導致總的吞吐量下降。CMS默認開啓的線程數量是(CPU數量+3)/4,當CPU數量較少時,對工作線程的影響可能就變得很大。這是一個問題

2.無法處理浮動垃圾。因爲CMS清除垃圾的操作是可以和工作線程併發執行的,所以在這期間工作線程產生的垃圾無法得到回收,這部分就是浮動垃圾。也正是因此,垃圾收集的時候還需要預留出一部分充足的空間給工作線程產生垃圾,所以說CMS不能像其他垃圾收集器一樣,等到老年代幾乎完全滿了才觸發GC,CMS默認設置是當老年代使用68%空間後就會激活GC,當然,如果老年代增長不是特別快,這個值可以適當調高。但是如果調得太高,沒有預留出足夠的空間存放工作線程產生的垃圾,就會產生一次“Concurrent Mode Failure”失敗,此時就會臨時啓用Serial Old收集器來重新進行老年代的收集,此時的停頓時間就更長了。

3.產生內存碎片:因爲是基於標記清理實現的,所以會產生大量內存碎片,若後續需要分配一個大對象,可能會由於內存不足分配失敗,提前引起GC

其他

G1收集器

G1收集器是當前收集器的最前沿成果之一,它是面向服務端應用的垃圾收集器,它的優點有:

1.多線程:多線程不僅僅是指垃圾收集操作可以併發,它指的是垃圾收集線程和工作線程可以併發,就和CMS一樣

2.分代收集:G1保存了分代概念,但它並沒有像傳統方法一樣將內存劃分爲新生代和老年代兩個區域。而是將內存劃分爲多個大小相等的區域,雖然依然有新生代和老年代的概念,但是它們之間不再是物理隔離的。

3.可預測的停頓:G1收集器一個很大的優勢就是能讓使用者明確指定在一個時間段內,垃圾收集工作不能超過一個限定的時間。實現方法是,G1會跟蹤每一個區域內垃圾堆積的價值大小,並在後臺維護一個優先列表,每次就可以選擇價值最高的一塊區域進行垃圾收集,保證了能在有限時間內達到儘可能高的效率

4.避免全堆掃描:G1收集器爲每一個區域都維護有一個叫Remember set的列表,當發生引用操作時,G1會判斷引用的對象是否在同一個區域內,若不在,那麼就要在對應區域的Remember set上記錄相關的引用信息。那麼在進行標記的時候,就可以避免全堆掃描,同時不發生遺漏。

內存分配策略

大對象優先在Eden區進行分配

當Eden區沒有足夠空間時,會引起一次minor GC

大對象直接進入老年代

大對象是指需要大量連續內存空間的Java對象,比如長字符串,或者數組。

爲了避免大對象在新生代間進行多次複製,所以大對象要直接進入老年代

-XX:PretenureSizeThreshold參數可以設置,大於該值的對象直接在老年代分配。

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

虛擬機給每個對象維護一個年齡計數器,當對象在Eden區出生,經歷第一次minor GC依然存活,並且可以被survivor區容納的話,對象年齡被設爲1,每熬過一次minor GC,年齡就+1.當年齡到達一個閾值,該對象就會晉升到老年代中,這個閾值可以通過-XX:MaxTenuringThreshold設置

動態對象年齡判定

在一些情況下,並不是一定要對象年齡超過閾值才能進入老年代。如果survivor區中,同年齡的所有對象大小超過了survivor內存的一半,年齡大於等於該年齡的對象可以直接進入老年代,無需等待達到閾值。

空間分配擔保

在發生Minor GC之前,虛擬機首先會查看老年代的最大可用連續空間是否大於新生代中所有對象總空間,如果是,則認爲這次minor GC是安全的。否則的話,如果允許擔保失敗的話,虛擬機再查看老年代中最大可用連續空間是否大於歷次晉升老年代所用內存的平均值,若是的話,就嘗試進行minor GC,儘管這次執行是有風險的,若執行失敗,則發生 Full GC;若不允許擔保失敗,此時會直接發生Full GC。

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