JVM垃圾回收算法(最全)


JVM垃圾回收算法(最全)

 

下面是JVM虛擬機運行時的內存模型:

 

1.方法區 Perm(永久代、非堆)

2.虛擬機棧

3.本地方法棧 (Native方法)

4.堆

5.程序計數器

 

1 首先的問題是:jvm如何知道那些對象需要回收 ?

目前兩種標識算法、三種回收算法、兩種清除算法、三種收集器

  • 引用計數法

每個對象上都有一個引用計數,對象每被引用一次,引用計數器就+1,對象引用被釋放,引用計數器-1,直到對象的引用計數為0,對象就標識可以回收

這個可以用數據算法中的圖形表示,對象A-對象B-對象C 都有引用,所以不會被回收,對象B由於沒有被引用,沒有路徑可以達到對象B,對象B的引用計數就就是0,對象B就會被回收。

 

 2

但是這個算法有明顯的缺陷,對於循環引用的情況下,循環引用的對象就不會被回收。例如下圖:對象A,對象B 循環引用,沒有其他的對象引用A和B,則A和B 都不會被回收。

 3

  • root搜索算法

這種算法目前定義了幾個root,也就是這幾個對象是jvm虛擬機不會被回收的對象,所以這些對象引用的對象都是在使用中的對象,這些對象未使用的對象就是即將要被回收的對象。簡單就是說:如果對象能夠達到root,就不會被回收,如果對象不能夠達到root,就會被回收。

如下圖:對象D訪問不到根對象,所以就會被回收

4

以下對象會被認為是root對象:

  • 被啟動類(bootstrap加載器)加載的類和創建的對象
  • jvm運行時方法區類靜態變量(static)引用的對象
  • jvm運行時方法去常量池引用的對象
  • jvm當前運行線程中的虛擬機棧變量表引用的對象
  • 本地方法棧中(jni)引用的對象

由於這種算法即使存在互相引用的對象,但如果這兩個對象無法訪問到根對象,還是會被回收。如下圖:對象C和對象D互相引用,但是由於無法訪問根,所以會被回收。

5

jvm在確定是否回收的對象的時候采用的是root搜索算法來實現。

在root搜索算法的里面,我們說的引用這里都指定的是強引用關系。所謂強引用關系,就是通過用new 方式創建的對象,並且顯示關聯的對象

[java]  view plain  copy
 
  1. <span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">Object obj = new Object();</span></span>  

以上就是代表的是強引用關系,變量obj 強引用了 Object的一個對象。

java里面有四種應用關系,從強到弱分別為:

Strong Reference(強引用) –>Weak Reference (弱引用) -> Soft Reference(軟引用) – > Phantom Reference(引用)

 

Strong Reference : 只有在引用對象root不可達的情況下才會標識為可回收,垃圾回收才可能進行回收

Weak Reference :即使在root算法中 其引用的對象root可達到,但是如果jvm堆內存 不夠的時候,還是會被回收。

Soft Reference : 無論其引用的對象是否root可達,在響應內存需要時,由垃圾回收判斷是否需要回收。

Phantom Reference :在回收器確定其指示對象可另外回收之后,被加入垃圾回收隊列.

 

 

  • 標記-清除

標記清除的算法最簡單,主要是標記出來需要回收的對象,然后然后把這些對象在內存的信息清除。如何標記需要回收的對象,在上一篇文章里面已經有說明。

2_1

 

  • 標記-清除-壓縮

這個算法是在標記-清除的算法之上進行一下壓縮空間,重新移動對象的過程。因為標記清除算法會導致很多的留下來的內存空間碎片,隨着碎片的增多,嚴重影響內存讀寫的性能,所以在標記-清除之后,會對內存的碎片進行整理。最簡單的整理就是把對象壓縮到一邊,留出另一邊的空間。由於壓縮空間需要一定的時間,會影響垃圾收集的時間。

2_2

 

  • 標記-清除-復制

這個算法是吧內存分配為兩個空間,一個空間(A)用來負責裝載正常的對象信息,,另外一個內存空間(B)是垃圾回收用的。每次把空間A中存活的對象全部復制到空間B里面,在一次性的把空間A刪除。這個算法在效率上比標記-清除-壓縮高,但是需要兩塊空間,對內存要求比較大,內存的利用率比較低。適用於短生存期的對象,持續復制長生存期的對象則導致效率降低

2_3 

由於現在的處理器都是多核的,處理器的性能得到了極大的提升,所以在此基礎上有產生了幾種垃圾收集算法。主要包括兩種算法

  • 並行標記清除

所謂並行,就是原來垃圾回收只是一個線程進行。現在創建多個垃圾回收線程。並行的進行標記和清除。比如把需要標記的對象平均分配到多個線程之后,當標記完成之后,多個線程進行清除。

 

  • 並發標記清除

所謂並發,就是應用程序和垃圾回收可以同時執行。在標記清除算法中,在標記對象和清除對象,以及壓縮對象的情況下是需要暫停應用的。那么並行標記清除壓縮算法則是在標記清除壓縮算法的基礎上,把標記清除壓縮算法分為以下幾個過程

初始標記->並發標記->重新標記->並發清除->重置

 

以上幾種算法是垃圾回收的基本算法,jvm垃圾回收就是在以上幾種算法為基礎的,在以上幾種算法的基礎上,java垃圾回收器可以分為以下幾種:

  • 串行收集器

用單線程處理所有垃圾回收工作,因為無需多線程交互,所以效率比較高。但是,也無法使用多處理器的優勢,所以此收集器適合單處理器機器

單線程收集器。在目前多核服務器端運行的情況下,效率比較低。比較適合堆內存小的情況下使用。

2_5

  • 並行收集器

用多線程處理所有垃圾回收工作,利用多核處理器的優勢。但是如果線程數量過多,導致線程之間頻繁調度,也會影響性能。一半並行收集的線程是處理器的個數。

“對吞吐量有高要求”,多CPU、對應用響應時間無要求的中、大型應用。舉例:后台處理、科學計算。

2_6

  • 並發收集器

並發收集器主要減少年老代的暫停時間,他在應用不停止的情況下使用獨立的垃圾回收線程,跟蹤可達對象。在每個年老代垃圾回收周期中,在收集初期並發收集器 會對整個應用進行簡短的暫停(初始標記的過程),在收集中還會再暫停一次。第二次暫停會比第一次稍長(重新標記的過程),在此過程中多個線程同時進行垃圾回收工作。

並發收集器使用處理器換來短暫的停頓時間。在一個N個處理器的系統上,並發收集部分使用K/N個可用處理器進行回收,一般情況下1<=K<=N/4。

在只有一個處理器的主機上使用並發收集器,設置為incremental mode模式也可獲得較短的停頓時間。

浮動垃圾:由於在應用運行的同時進行垃圾回收,所以有些垃圾可能在垃圾回收進行完成時產生,這樣就造成了“Floating Garbage”,這些垃圾需要在下次垃圾回收周期時才能回收掉。所以,並發收集器一般需要20%的預留空間用於這些浮動垃圾。

Concurrent Mode Failure:並發收集器在應用運行時進行收集,所以需要保證堆在垃圾回收的這段時間有足夠的空間供程序使用,否則,垃圾回收還未完成,堆空間先滿了。這種情況下將會發生“並發模式失敗”,此時整個應用將會暫停,進行垃圾回收。

並發收集器,在垃圾回收的時候采用並發標記清除算法的收集器

對響應時間要求高的,多CPU,大型應用。比如頁面請求/web服務器。前端業務系統用的比較多。

2_7

 

串行處理器:

--適用情況:數據量比較小(100M左右);單處理器下並且對響應時間無要求的應用。

--缺點:只能用於小型應用

並行處理器:

--適用情況:“對吞吐量有高要求”,多CPU、對應用響應時間無要求的中、大型應用。舉例:后台處理、科學計算。

--缺點:垃圾收集過程中應用響應時間可能加長

並發處理器:

--適用情況:“對響應時間有高要求”,多CPU、對應用響應時間有較高要求的中、大型應用。舉例:Web服務器/應用服務器、電信交換、集成開發環境。

JDK5.0適用的分代垃圾回收算法

       分代的垃圾回收策略,是基於這樣一個事實:不同的對象的生命周期是不一樣的。因此,不同生命周期的對象可以采取不同的收集方式,以便提高回收效率。

       在Java程序運行的過程中,會產生大量的對象,其中有些對象是與業務信息相關,比如Http請求中的Session對象、線程、Socket連接,這類對象跟業務直接掛鈎,因此生命周期比較長。但是還有一些對象,主要是程序運行過程中生成的臨時變量,這些對象生命周期會比較短,比如:String對象,由於其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次即可回收。

       試想,在不進行對象存活時間區分的情況下,每次垃圾回收都是對整個堆空間進行回收,花費時間相對會長,同時,因為每次回收都需要遍歷所有存活對象,但實際上,對於生命周期長的對象而言,這種遍歷是沒有效果的,因為可能進行了很多次遍歷,但是他們依舊存在。因此,分代垃圾回收采用分治的思想,進行代的划分,把不同生命周期的對象放在不同代上,不同代上采用最適合它的垃圾回收方式進行回收。

如何分代

如圖所示:

       虛擬機中的共划分為三個代:年輕代(Young Generation)、年老點(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java類的類信息,與垃圾收集要收集的Java對象關系不大。年輕代和年老代的划分是對垃圾收集影響比較大的。

年輕代:

      所有新生成的對象首先都是放在年輕代的。年輕代的目標就是盡可能快速的收集掉那些生命周期短的對象。年輕代分三個區。一個Eden區,兩個Survivor區(一般而言)。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被復制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活對象將被復制到另外一個Survivor區,當這個Survivor去也滿了的時候,從第一個Survivor區復制過來的並且此時還存活的對象,將被復制“年老區(Tenured)”。需要注意,Survivor的兩個區是對稱的,沒先后關系,所以同一個區中可能同時存在從Eden復制過來 對象,和從前一個Survivor復制過來的對象,而復制到年老區的只有從第一個Survivor去過來的對象。而且,Survivor區總有一個是空的。同時,根據程序需要,Survivor區是可以配置為多個的(多於兩個),這樣可以增加對象在年輕代中的存在時間,減少被放到年老代的可能。

年老代:

      在年輕代中經歷了N次垃圾回收后仍然存活的對象,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命周期較長的對象。

持久代:

      用於存放靜態文件,如今Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些class,例如Hibernate等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。持久代大小通過-XX:MaxPermSize=&lt;N>進行設置。

什么情況下觸發垃圾回收

由於對象進行了分代處理,因此垃圾回收區域、時間也不一樣。GC有兩種類型:Scavenge GC和Full GC。

 

(1)新生代上的GC實現
 
Serial:單線程的收集器,只使用一個線程進行收集,並且收集時會暫停其他所有
工作線程(Stop the world)。它是Client模式下的默認新生代收集器。
 
ParNew:Serial收集器的多線程版本。在單CPU甚至兩個CPU的環境下,由於線程
交互的開銷,無法保證性能超越Serial收集器。
 
Parallel Scavenge:也是多線程收集器,與ParNew的區別是,它是 吞吐量優先
收集器。吞吐量=運行用戶代碼時間/(運行用戶代碼+垃圾收集時間)。另一點區別
是配置-XX:+UseAdaptiveSizePolicy后,虛擬機會自動調整Eden/Survivor等參數來
提供用戶所需的吞吐量。我們需要配置的就是內存大小-Xmx和吞吐量GCTimeRatio。
 
(2)老年代上的GC實現
 
Serial Old:Serial收集器的老年代版本。
 
Parallel Old:Parallel Scavenge的老年代版本。此前,如果新生代采用PS GC的話,
老年代只有Serial Old能與之配合。現在有了Parallel Old與之配合,可以在注重吞吐量
及CPU資源敏感的場合使用了。
 
CMS:采用的是 標記-清除而非標記-整理,是一款並發低停頓的收集器。但是由於
采用標記-清除,內存碎片問題不可避免。可以使用-XX:CMSFullGCsBeforeCompaction
設置執行幾次CMS回收后,跟着來一次內存碎片整理。

 

 

GC類型 
GC有兩種類型:Scavenge GC和Full GC。 

1. Scavenge GC 

一般情況下,當新對象生成,並且在Eden申請空間失敗時,就好觸發Scavenge GC,堆Eden區域進行GC,清除非存活對象,並且把尚且存活的對象移動到Survivor區。然后整理Survivor的兩個區。 
2. Full GC 
對整個堆進行整理,包括Young、Tenured和Perm。Full GC比Scavenge GC要慢,因此應該盡可能減少Full GC。有如下原因可能導致Full GC: 
* Tenured被寫滿 
* Perm域被寫滿 
* System.gc()被顯示調用 
* 上一次GC之后Heap的各域分配策略動態變化 

 

 

分代垃圾回收流程 

分代垃圾回收流程 

分代垃圾回收流程 

分代垃圾回收流程

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM