Java 虛擬機 垃圾回收

如何判斷對象已是垃圾

引用計數法

引用計數法實現簡單,判定效率也很高,但是JAVA虛擬機並沒有用引用計數法來判斷對象是否存活

原理:給對象中添加一個引用計數器,每當一個地方引用他的時候,計數器的值就加一,當引用失效時就減一,任何時刻計數器爲0時對象就是不可能被使用的。
缺點:對於兩個互相引用的對象無法當做垃圾收集,雖然他們再無任何引用。

可達性分析算法

原理:

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

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

缺點:

實現比較複雜;
需要分析大量數據,消耗大量時間;
分析過程需要GC停頓,即停頓所有JAVA執行線程。

引用:

在jdk 1.2之前對於引用的定義就是如果一個reference類型的數據中儲存着另一個內存的起始地址就稱作引用。
這種定義太過狹隘,一個對象只有被引用和不被引用兩種狀態,對於如何描述一些“食之無味,唾之可惜”的對象就顯得無能爲力。我們希望在內存充足的情況下保留這些對象,在不充足的情況下拋棄這些對象。
在jdk1.2之後,Java對引用的概念進行了擴充,將引用分爲了強引用,軟引用,弱引用,虛引用這四種。

強引用:

Object object = new Object();
只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象

軟引用:

描述一些還有用但非必須的對象,在系統將要發生內存溢出異常前,將會把這些對象列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的內存纔會拋出內存異常,1.2之後提供了SoftReference類來實現軟引用。

弱引用:

弱引用也是用來描述非必須對象的,但是它的強度比軟引用更弱一些,內弱引用關聯的對象只能生存到下一次垃圾回收發生之前,當垃圾收集器工作時,無論當前內存是是否足夠,都會回收掉只被弱引用關聯的對象。1.2之後提供了weak類來實現弱引用。

虛引用:

他是最弱的一種引用一個對象是否有虛引用的存在,完全不會對其生存時間構成引用關聯的唯一目的就是能在這個對象被垃圾回收器時收到一個系統通知。jdk1.2後提供了Phantom Reference 類來實現虛引用。

finalize()方法

在可達性分析算法中不可達的對象,也並非是“非死不可”的,他們距離真正的死亡至少要經歷兩次標記過程:

1.第一次標記

在可達性分析後發現GC Roots沒有任何引用1鏈相鏈時,被第一次標記;
並進行一次篩選; 此對象有沒有必要執行finalize()方法;

  • A 沒有必要執行
    + 對象沒有覆蓋finalize()方法;
    + finalize()方法已經被JVM調用過;
    這兩種情況就可以認爲對象已死,可以回收;
  • B 有必要執行
    對於有必要執行finalize()方法的對象,被放入F-Queue隊列中;
    稍後再JVM自動建立,低優先級的Finalizer線程(可能多個線程)中觸發這個方法;

2 第二次標記

GC將對F-Queue隊列中的對象進行第二次小規模標記:
finalize()方法是對象逃脫死亡的最後機會:

  • A 如果對象在其finalize()方法中重新與引用鏈上的任何一個對象建立關聯,第二次標記時會將其移出“即將回收”的集合
  • B 如果對象沒有,也可認爲對象已死,可以回收了

一個對象的finalize()方法只會被系統自動調用一次,經過finalize()方法逃脫的對象,第二次不會再調用;

finalize()是Object類的一個方法,是Java剛誕生時爲了使C/C++程序員容易接受它所做出的一個妥協,但不要當作類似C/C++的析構函數;

HotSpot虛擬機中的對象可達性分析的實現

問題:

1,消耗大量時間:
GC Roots 主要在全局性的引用(常量或靜態屬性)
和執行上下文(棧幀中的本地變量表);
要在這些大量的數據中,逐個檢查引用,會消耗很多時間;
2,GC停頓
可達性分析期間需要保證整個執行系統的一致性,對象的引用關係不能發生變化;
導致GC進行時必須停頓所有JAVA執行線程(“Stop the world”);
幾乎不會發生停頓的CMS收集器中,枚舉根節點時也必須停頓
Stop the World:
是JVM 在後臺自動發起和自動完成的;
在用戶不可見的情況下,把用戶正常的線程全部停掉;

枚舉根節點:

枚舉根節點也就是查找GC Roots:
目前主流的JVM都是準確式GC,可以直接得知哪些地方存放着對象引用,所以執行系統停頓下來後,並不需要全部、逐個檢查全局性和執行上下文中的引用位置

在Hotspot中,是使用一組叫OopMap的數據結構來達到這個目的;
在類加載時,計算對象內什麼偏移量上是什麼類型的數據
在JIT編譯時,也會記錄棧和寄存器中的哪些位置是引用;
這樣GC掃描時就可以直接得知這些信息;

安全點 :

1.HotSpot 在Oopmap的幫助下可以準確的完成GC Roots的枚舉,但是這有一個問題:
運行中,非常多的指令會導致引用關係變化;
如果這些指令都生成對應的OopMap,需要的空間成本太高;

問題解決:
只在特定的位置記錄OopMap引用關係,這些位置叫安全點;
所以程序執行時並非所有地方都能停頓下來開始GC

2.安全點的選定:

指令序列複用:
方法調用,循環跳轉,循環的末尾,異常跳轉等,
只有這些功能的指令纔會產生Safepoint;

  1. 如何在安全點上停頓
    • A.搶佔式中斷(Preemptive Suspension)
      + 在GC發生時,首先中斷所有線程;
      + 如果發現不在Safepoint上的線程,就恢復讓其運行到Safepoint上;
    • B 主動式中斷(Voluntary Suspension)
      + 在GC發生時,不直接操作線程中斷,而是僅簡單的設置標誌
      + 讓各個線程執行時主動去輪詢這個標誌,發現中斷標誌位真時就自己中斷掛起;而輪詢標誌的地方是和SafePoint是重合的;
      在JIT執行方式下:test指令是HotSpot生成的輪詢指令

回收方法區

永久代的垃圾收集主要回收兩部分內容:
廢棄常量和無用的類。
回收廢棄常量與回收Java堆中的對象非常類似,“abc”,如果沒有一個String對象叫做“abc”時,就會被系統清理出常量池。
判斷一個類是否是無用的類的條件則相對苛刻:
**+ 該類的所有實例都已經被回收,在Java堆中不存在該類的任何實例

  • 加載該類的ClassLoader已經被回收。
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。**
    虛擬機可以對滿足上述三個無用的類進行回收,不是必然,
    HotSpot虛擬機提供了-Xnoclassgc參數來進行控制,還可以使用-verbose:class 以及-XX:+TraceClassLoading,-XX:+TraceClassUnLoading查看類加載和卸載信息,在大量使用反射,動態代理,這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。

垃圾收集算法

標記-清除算法:

標出所有要回收的對象,然後統一回收,

這兩個步驟的效率並不高,之後會產生大量的不連續的內存碎片,空間碎片太多可能會導致以後分配大對象時,無法找到足夠的內存而不得不提前觸發另一次垃圾收集動作。

複製算法

將內存劃分爲兩塊,每次只使用一塊,當這塊用完了,將存活着的對象複製到另一塊內存上去,然後將使用過的一塊清空
不用考慮內存碎片等情況,只要一動堆頂指針,按順序分配內存就ok

JVM 沒有按照1:1來劃分內存空間,而是將內存劃分爲較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor空間,當回收時,將Eden和Survivor中還存活的對象一次性複製到另外一塊Survivor空間上,最後清理掉Eden和Survivor空間。
Hotspot默認Eden和Survivor的空間比例是8:1,也就是新生代中可用內存是整個新生代的90%。

標記-整理算法

複製收集算法在對象存活率較高時就要進行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間就要有額外的空間分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以老年代一般不能直接選用這種算法。
在這裏插入圖片描述

分代收集算法:

新生代,每次收集垃圾時都有大量的對象死去,只有少量存活,就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集,而老年代中因爲對象存活率高,沒有額外的空間對他進行分配擔保,就必須使用“標記-清理”或者“標記-整理”算法來進行回收。

垃圾收集器

Serial收集器

他是最基本的收集器,在JDK1.3.1之前是虛擬機新生代收集的唯一選擇,這個收集器時一個單線程去完成垃圾收集工作的,
在他進行垃圾收集時必須暫停其他所有的工作線程,直到他收集結束,在用戶不可見的情況下將用戶正常工作的線程全部停掉。
優點:
簡單高效,對於限定單個CPU的環境來說,Serial收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率,在用戶桌面應用場景中,需要的內存一般來說不會很大,收集幾十兆甚至幾百兆的新生代,停頓時間完全可以控制在集市毫秒最多一百多毫秒之內,只要不是頻繁發生,這點停頓是可以接受的,所以Serial收集器對於運行在Client模式下的虛擬機來說是一個很好的選擇

ParNew 收集器

ParNew收集器除了多線程收集外,其他與Serial收集器沒有不同,
他是許多運行在Server模式下的虛擬機首選的新生代收集器,其中有一個與性能無關的很重要的原因就是,除了Serial收集器外,目前只有他能與CMS收集器配合工作。在JDK1.5時期,HOtSpot推出了一款強交互應用中很強的垃圾收集器--------CMS收集器(Concurrent Mark Sweep),可以讓垃圾收集線程和用戶線程同時工作。
CMS作爲老年代的收集器無法與JDK1.4.0中已經存在的新生代收集器Parallel Scavenger 配合工作,所以在JDK1.5.0中使用的CMS收集老年代時,新生代只能選擇ParNew或者Serial收集器中的一個。
ParNew收集器也是使用-XX:+UseConcMarkSweepGC選項後的默認新生代收集器,也可以使用-XX:+UseParNewGC選項來強制指定它。
ParNew在單核CPU的效果絕對沒有Serial收集器好但是隨着可以使用的Cpu數量的增加,他對於GC時系統資源的有效利用還是有好處的,他默認開啓的收集線程數與CPU的數量相同,在CPU非常多的情況下,可以使用-XX:ParallelGCThreads參數來限制垃圾回收的線程數。

Parallel Scavenger收集器

他是一個新生代收集器,他也是使用複製算法的收集器,又是並行的多線程收集器
他的關注點與其他收集器不同,CMS等收集器關注點是儘可能的縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenger收集器的目的是達到一個可控制的吞吐量,
吞吐量就是CPU用於運行用戶代碼與CPU總消耗的時間的比值,即吞吐量 = 運行用戶代碼時間/(運行用戶代碼時間 + 垃圾收集時間)
總共100min,垃圾收集1min,那吞吐量就是99%
停頓時間越短越適合需要與用戶進行交互的程序
而高吞吐量則可以高效地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不需要太多的交互任務。
Paralllel Scavenger提供了兩個參數用於精準控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數,

MaxGCPauseMillis參數允許的值是一個大於0的毫秒數收集器就儘可能保證內存垃圾回收的時間在這個範圍內。
但你不要以爲把這個參數設置的稍小一點就能使得垃圾收集的速度變得更快,GC停頓時間的縮短是以犧牲吞吐量和新生代空間來換取的:系統把新生代調小一些,這也導致收集發生的更頻繁,原來十秒收集一次每次停頓100ms,現在變成每5秒收集一次,每次停頓70ms,停頓的時間確實下降了,但是吞吐量也降下來了。

GCTimeRatio參數的值應該是一個大於0且小於100的整數,也就是垃圾收集時間佔總時間的比率

由於與吞吐量關係密切,Parallel Scavenger 收集器也經常稱爲“吞吐量優先”收集器。
他還有一個參數:-XX: +UseAdaptiveSizePolicy,這是一個快關參數,當這個參數打開後,就不需要手工指定新生代的大小(-Xmn),Eden與Survivor區的比例(-XX:SurvivorRatio),晉升老年代對象年齡(-XX:PretenureSizeThreshold)等參數細節了,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式叫
FC自適應的調節策略(GC Ergonomics)

Serial Old收集器

Serial old收集器是Serial收集器的老年版本,他同樣是一個單線程收集器,使用“標記-整理”算法。這個收集器的主要意義也是給Client模式下的虛擬機使用。如果在Server模式下,他還有兩大用途

  • 在1.5之前的版本中與Parallel Scavenger收集器搭配使用,
  • 作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。

Parallel Old收集器

Parallel Old是Parallel Scavenger收集器的老年代版本,使用多線程和“標記-整理”算法。這個收集器於JDK1.6之後開始提供。
在此之前新生代的Parallel Scavenger 收集器一直處於比較尷尬的狀態,因爲,新生代如果選擇了Parallel Scavenger 收集器,老年代除了Serial Old收集器外別無選擇。由於老年代SerialOld收集器在服務端應用性能上的“拖累”,使用了Parallel Scavenger收集器也未必能在整體應用上獲得吞吐量最大化的效果,
由於單線程的老年代收集中無法利用服務器多cpu的處理能力,在老年代很大而且硬件比較高級的環境中,這種組合的吞吐量沒有ParNew加上CMS給力
Parallel Old收集器加上Parallel Scavenger收集器應用於注重吞吐量以及CPU資源敏感的場合。

CMS收集器

CMS是一種以獲取最短回收停頓時間爲目標的收集器,適用於互聯網站或者B/s系統的服務端上,這類系統尤其注重服務的響應速度系統停頓的時間最短,以給用戶帶來更好地體驗
CMS收集器基於“標記-清除”算法,整個過程分爲四個步驟

  • 初始化標記
  • 併發標記
  • 重新標記
  • 併發清除
    初始標記和重新標記仍然需要"Stop the World"。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,併發標記階段就是進行GC RootsTracing的過程,而重新標記階段是爲了修正併發標記期間因用戶程序繼續操作而導致標記產生變動的那一部分的標記記錄,這個階段的停頓時間會比初始標記階段稍長一些,但遠比並發標記的時間短
    CMS收集器內存回收過程是與用戶線程一起併發執行的,併發標記和併發清除過程都可以於用戶線程一起工作。
    缺點:
  • 對CPU資源非常敏感,因爲他佔用了一部分的cpu資源而導致應用程序變慢,總吞吐量hi降低。CMS默認啓動的回收線程數是(CPU數量+3)/ 4,也就是當CPU數量在四個以上時,併發回收時垃圾線程不少於25%的cpu資源,並且隨着CPU數量的增加而下降。但是當CPU不足四個時,CMS對用戶程序的影響就可能變得很大,
    爲了應對這種情況,虛擬機提供了一種稱爲“增量式併發收集器”的CMS收集器變種所做的事情就是在併發標記,清理時讓GC線程,用戶線程交替運行,儘量減小GC線程的獨佔資源時間,這樣垃圾收集的時間會更長但對用戶程序的影響就會顯得更少一些,但是整個垃圾回收的過程會更長。
  • CMS無法處理浮動垃圾。可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生,這一部分垃圾出現在標記過程後,CMS無法在當次收集中處理掉他們,只好留在下一次GC時再清理掉。Concurrent Mode Failure這一部分就叫浮動垃圾,也是由於垃圾線程運行時用戶線程還需要運行,那就還需給用戶線程留夠空間,因此CMS不會像其他收集器那樣等到老年代幾乎全部填滿了再進行收集,需要預留一部分空間提供併發收集時程序運行做使用。在JDK1.5的默認配置下,CMS收集器當老年代使用了68%的空間後就會被激活,如果在應用中老年代的增長不是太快,可以適當調高參數-XX:CMSInitiatingOccupancyFraction的值來提高觸發百分比,以便降低內存回收的次數從而獲取更好的性能,在JDK1.6中,CMS收集器的啓動閾值已經提高到92%,要是CMS運行期間預留的內存無法滿足程序需要,就會出現一次Concurrent Mode Failure 失敗,這時虛擬機將啓動後備預案:啓動臨時Serial Old收集器來重新收集老年代,這樣停頓的時間就很長了,所以說參數-XX:CMSInitiatingOccupancyFraction設置得太高很容易導致大量的“Concurrent Mode Failure”失敗,性能反而降低。
  • CMS基於“標記-清除”算法實現的收集器,會產生大量的空間碎片,碎片過多,會給對象的分配帶來很多麻煩,往往會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間分配,不得提前觸發一次FullGc,爲了解決這一問題,CMS 收集器提供了一個 -XX:+UseCMSCompactAtFullCollection開關參數(默認是開啓的),用於在CMS頂不住的時候開啓內存碎片的合併整理過程,內存整理的過程是無法併發的,空間碎片問題沒有了,但停頓時間不得不變長。虛擬機設計者還提供了另外一個參數:-XX:CMSFullGCsBeforeCompaction,這個參數用於設置執行多少次不壓縮的Full GC後,接着來一次壓縮(默認值是0,表示每一次進入Full GC時都進行碎片整理)。

G1收集器

G1收集器時當今收集器的最牛逼的成果之一,是面向服務端應用的垃圾收集器。
特點:

  • 並行和併發:G1能充分利用多CPU,多核環境下的硬件優勢,使多個CPU來縮短Stop The World停頓時間,部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過併發的方式讓java程序繼續執行。
  • 分代收集:與其他收集器一樣,分代概念在G1中保留,雖然G1可以不需要其他垃圾收集器的配合就能獨立管理整個GC堆,但它能夠採用不同的方式去處理新創建的對象和已經存活一段時間,熬過的多次GC的舊對象以獲取更好地收集效果。
  • 空間整合:與CMS的“標記-整理”算法不同,G1從整體來看是基於“標記-整理”算法實現的收集器,從局部來看是基於“複製”算法實現的,但無論如何,這兩種算法都一位着G1運作期間不會產生內存碎片,收集後能提供規整的可用內存。這種特性有利於程序長時間運行,分配大對象時不會因爲無法找到連續內存空間而提前觸發下一次GC。
  • 可預測的停頓:這是G1相對CMS的另一大優勢,降低停頓時間是G1和CMS共用的關注點,但G1除了追求低停頓外,還能建立可預測停頓時間模型,能讓使用者確定在一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這似乎已經是Java的垃圾收集器的特徵了

別的垃圾收集器進行垃圾收集的範圍都是整個新生代或者老年代,而G1不再是這樣,他將整個java堆劃分爲多個大小相等的獨立區域,保留有新生代和老年代的概念,但是新生代和老年代不再是物理隔離的了,他們都是一部分Region(不連續)的集合。
G1收集器之所以能建立可預測的停頓時間模型,是因爲他可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region(這也就是Garbage-First的來由),這種使用Region劃分內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內可以獲取儘可能高的收集效率。

G1的實現:

Region不可能是孤立的,一個對象分配在某個Region中不可能只和本Region中的對象引用,而是可以和多個Java堆中任意對象發生引用關係,那麼做可達性分析的時候不可能再把整個java堆來掃一遍來確保對象是否存活。
在G1收集器中,Region之間的對象引用以及其他收集器中的新生代與老年代之間的對象引用,虛擬機是利用Remember Set來避免全對掃描的,G1中每個Region都有一個與之對應Remember Set,虛擬機發現在對Reference 類型的數據進行寫操作時,會產生一個Write Barrier 暫停中斷寫操作,檢查Reference 引用的對象是否處於不同的Region之間(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),如果是,便通過CardTable把相關引用信息記錄到被引用對象所屬的Region 的RememberSet之中。當進行內存回收時,在GC根節點的枚舉範圍中加入Remembered Set之中。當進行內存回收時,在GC更節點的枚舉範圍中加入Remember Set即可保證不對全堆掃描也不會有遺漏。

G1的運行步驟:

  • 初始標記
  • 併發標記
  • 最終標記
  • 篩選回收
    初始標記階段僅僅是標記一下GC Roots 能關聯到的對象,並且修改 TaMs的值,讓下一階段用戶程序併發執行時,能在正確的Region中創建新對象,這階段需要停頓線程,但耗時很短。
    併發標記階段是從GCRoots開始對堆中對象進行可達性分析,找出存活的對象,這一階段耗時很長,但可與用戶程序併發執行
    最終標記階段是爲了修正在併發標記期間因用戶程序操作而導致標記產生變動的那一部分記記錄,虛擬機將這段時間對象變化記錄在線程Remember Set logs裏面,最終標記階段需要把Remember Set Logs 的數據合併到Remember Set中,這階段需要停頓線程,但是可並行執行,最後在篩選回收階段首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計劃,
發佈了124 篇原創文章 · 獲贊 9 · 訪問量 2472
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章