日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > java >内容正文

java

Java垃圾收集机制

發(fā)布時間:2023/11/30 java 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java垃圾收集机制 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

本文是《深入理解Java虛擬機(jī)》一書中第三章的讀書總結(jié)。

前面介紹了Java內(nèi)存結(jié)構(gòu)和HotSpot虛擬機(jī)在堆內(nèi)存中管理對象的過程。不過,在Java程序中對象的創(chuàng)建是非常頻繁的,而內(nèi)存的大小又是有限的,為了內(nèi)存的重復(fù)利用,就需要對內(nèi)存中的對象進(jìn)行垃圾收集。其實(shí),這也是Java和C++的一個區(qū)別,在Java中可以進(jìn)行自動的垃圾收集,而C和C++中需要程序員手動回收不再使用的對象。

Java中的垃圾收集是虛擬機(jī)要考慮的問題。那么以虛擬機(jī)的角度考慮,如果要收集虛擬機(jī)內(nèi)存中的垃圾,需要考慮哪些問題呢?

  • Java虛擬機(jī)中的內(nèi)存分為程序計數(shù)器、虛擬機(jī)棧、本地方法棧、Java堆和方法區(qū)等幾部分,在哪些部分回收內(nèi)存呢?
  • 確定了要回收的內(nèi)存,內(nèi)存中必然存在著很多內(nèi)容,如何判定這些內(nèi)容就是不需要的垃圾了呢?
  • 程序不斷運(yùn)行,垃圾收集不可能也隨著程序一直運(yùn)行,那什么時候進(jìn)行垃圾收集操作呢?
  • 最重要的問題是,怎么回收?

Java的垃圾收集機(jī)制是一個挺復(fù)雜的過程,涉及到的內(nèi)容也很多,上面的問題一個一個解決。

1、回收區(qū)域

在前面幾篇中可以知道,Java內(nèi)存中的程序計數(shù)器、虛擬機(jī)棧和本地方法棧是線程私有的,線程結(jié)束也就沒了。其中程序計數(shù)器負(fù)責(zé)指示下一條指令,棧中的棧幀隨著方法的進(jìn)入和退出不停的入棧出棧。每一個棧幀的大小在編譯時就基本已經(jīng)確定。所以這幾個區(qū)域就不需要考慮內(nèi)存回收,因為方法結(jié)束或線程停止,內(nèi)存就回收了。

和上述三個區(qū)域不同的是,Java堆和方法區(qū)是線程共享的。在Java堆中存放著所有線程在運(yùn)行時創(chuàng)建的對象,在方法區(qū)中存放著關(guān)于類的元數(shù)據(jù)信息。我們在程序運(yùn)行時才能確定需要加載哪些類的元數(shù)據(jù)信息到方法區(qū),創(chuàng)建哪些對象到堆中,也就是說,這部分的內(nèi)存分配和回收都是動態(tài)的。也因為這樣,這兩個部分是垃圾收集器所關(guān)注的地方。

2、誰才是垃圾?

首先考慮一下存放對象的Java堆。

程序中創(chuàng)建的對象絕大多數(shù)都會在Java堆中,而程序的運(yùn)行也會創(chuàng)建大量的對象。但這些對象并不總是使用,這樣就產(chǎn)生了一些不會再使用的垃圾。這些垃圾占據(jù)著寶貴的內(nèi)存空間,所以需要回收這些空間。不過,怎么才能確定堆中的對象是垃圾呢?

一種常見的算法是引用計數(shù)算法,它基于這樣的考慮,給對象添加一個引用計數(shù)器,每當(dāng)有一個地方引用它時,計數(shù)器的值就加1;當(dāng)引用實(shí)效時,計數(shù)器的值就減1。當(dāng)計數(shù)器的值為0時,對象就不可能再被使用。

引用計數(shù)算法實(shí)現(xiàn)簡單,判定效率也高。不過,主流的Java虛擬機(jī)中并沒有使用引用計數(shù)算法來管理內(nèi)存,因為這個算法很難解決對象之間相互循環(huán)引用的問題。

考慮下面的代碼:

public class ReferenceCountingGC {public Object instance=null;private static final int _1mb=1024*1024;@SuppressWarnings("unused")private byte[] bigSize=new byte[2*_1mb];public static void testGC() {ReferenceCountingGC objA=new ReferenceCountingGC();ReferenceCountingGC objB=new ReferenceCountingGC();objA.instance=objB;objB.instance=objA;objA=null;objB=null;System.gc();}public static void main(String[] args) {ReferenceCountingGC.testGC();} }
ReferenceCountingGC類中有一個實(shí)例對象,在測試代碼中構(gòu)造的兩個對象objA和objB在instance實(shí)例對象上互相引用。這樣,每個對象的引用計數(shù)都是2,當(dāng)兩個對象都賦值為null時,引用計數(shù)減1變?yōu)?,按照引用計數(shù)算法這不是垃圾。但是,很明顯這兩個對象已經(jīng)不能再被訪問到了,這就是垃圾。實(shí)際上,運(yùn)行之后Java虛擬機(jī)將這兩個對象作為垃圾回收掉了。

那么Java中使用的是什么方法呢?是可達(dá)性分析算法(Reachability Analysis)。

可達(dá)性分析算法的基本思想是通過一系列稱為“GC Roots”的對象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當(dāng)一個對象到GC Roots沒有任何引用鏈相連時,即從GC Roots到這個對象不可達(dá),就說明這個對象是不可用的。如下圖,左面的四個對象都有引用鏈到GC Roots,因此是可用的;右面的三個對象到GC Roots不可達(dá),所以是不可用的。


在Java中,下面幾種對象可以作為GC Roots:

  • 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象;
  • 方法區(qū)中類靜態(tài)屬性引用的對象;
  • 方法區(qū)中常量引用的對象;
  • 本地方法棧中JNI(即Native方法)引用的對象;

其實(shí)這兩種方法都涉及到了對象的引用,也就是說對象是否是垃圾都與引用有關(guān),因此有必要全面的理解一下Java中的引用。

其實(shí)Java中的引用一共有四種。這是JDK 1.2 之后對引用概念的擴(kuò)充,分別是強(qiáng)引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference),這四種引用的強(qiáng)度依次逐漸減弱。

(1)強(qiáng)引用

強(qiáng)引用就是程序中普遍存在的,類似“Object obj=new Object()”這類的引用,只要強(qiáng)引用還存在,垃圾回收器就不會回收被引用的對象。

(2)軟引用

軟引用用來描述一些還有用但不是必須的對象。對于軟引用關(guān)聯(lián)的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會把這些對象列入回收范圍進(jìn)行第二次回收。如果這次回收還沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常。SoftReference類來實(shí)現(xiàn)軟引用。

(3)弱引用

弱引用也用來描述非必須的對象,但是強(qiáng)度比軟引用還弱,被引用的對象只能存活到下一次垃圾收集之前。當(dāng)下一次垃圾收集器工作時,不論內(nèi)存是否足夠,都會回收這些對象。WeakReference類實(shí)現(xiàn)了弱引用。

(4)虛引用

虛引用是最弱的一種引用,也叫幽靈引用或幻影引用。一個對象是否有虛引用存在不會對其生存時間產(chǎn)生影響,也無法通過虛引用來取得一個對象實(shí)例。虛引用的唯一目的就是當(dāng)被虛引用關(guān)聯(lián)的對象被收集器收集時收到一個系統(tǒng)通知。PhantomReference類實(shí)現(xiàn)了虛引用。

3、垃圾也有可能變廢為寶

垃圾也有可能變廢為寶然后再利用呢。其實(shí),即使在可達(dá)性分析算法中不可達(dá)的對象,也并非是“非死不可”的,要真正認(rèn)為一個對象是垃圾要收集,至少要經(jīng)過兩次標(biāo)記過程:如果對象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)不可達(dá),那么就將它進(jìn)行第一標(biāo)記并進(jìn)行一次篩選,篩選的條件是這個對象是否有必要執(zhí)行finalize()方法。當(dāng)對象沒有覆蓋finalize方法,或finalize方法已經(jīng)被虛擬機(jī)執(zhí)行過了,虛擬機(jī)任何沒有必要執(zhí)行finalize方法。

如果這個對象被判定為有必要執(zhí)行finalize方法,那么這個對象會放置在一個叫做F-Queue的隊列中,并在稍后由一個由虛擬機(jī)自動建立的、低優(yōu)先級的Finalizer線程去執(zhí)行。不過虛擬機(jī)只是會觸發(fā)這個方法,但不承諾會等待執(zhí)行完畢,這是因為,如果一個對象的finalize方法執(zhí)行緩慢,或發(fā)生了死循環(huán),就會導(dǎo)致F-Queue對象中的其他對象處于等待,甚至整個垃圾收集系統(tǒng)崩潰。稍后GC會在F-Queue中的對象進(jìn)行第二次小規(guī)模的標(biāo)記,如果這時標(biāo)記為可達(dá),就可以不被收集;如果仍然不可達(dá),那么就被標(biāo)記為垃圾了。具體的流程圖如下:


下面的代碼演示了上面所說的內(nèi)容。

public class FinalizeEscapeGC {public static FinalizeEscapeGC SAVE_HOOK=null;public void isAlive(){System.out.println("yes,i am still alive.");}protected void finalize()throws Throwable{super.finalize();System.out.println("finalize method executed!");FinalizeEscapeGC.SAVE_HOOK=this;}public static void main(String[] args) throws InterruptedException {SAVE_HOOK=new FinalizeEscapeGC();SAVE_HOOK=null;System.gc();Thread.sleep(500);if(SAVE_HOOK!=null){SAVE_HOOK.isAlive();}else{System.out.println("no,i am dead.");}SAVE_HOOK=null;System.gc();Thread.sleep(500);if(SAVE_HOOK!=null){SAVE_HOOK.isAlive();}else{System.out.println("no,i am dead.");}} }
結(jié)果如下:


FinalizeEscapeGC類覆蓋了finalize方法,所以在GC將SAVE_HOOK第一次標(biāo)記為垃圾后的篩選中認(rèn)為finalize有必要執(zhí)行。在覆蓋的finalize方法中,將自己賦值給了類的變量SAVE_HOOK,成功拯救自己,第一次沒有被收集。但是第二次雖然代碼相同,但是由于虛擬機(jī)已經(jīng)執(zhí)行過finalize方法了,GC不認(rèn)為有必要執(zhí)行,在第二次標(biāo)記中也標(biāo)記為垃圾,所以沒有能拯救自己,被當(dāng)做垃圾收集了。

4、回收方法區(qū)

除了Java堆,方法區(qū)中也存在垃圾收集。只不過這里的收集效率比較低。

方法區(qū),在HotSpot虛擬機(jī)中叫永久代,GC收集兩部分內(nèi)容,廢棄常量和無用的類。收集廢棄常量與收集Java堆中的對象類似。以常量池中字面量的收集為例,假如一個字符串“ABC”已經(jīng)在常量池中,但是當(dāng)前系統(tǒng)中沒有任何一個String對象是“ABC”,即沒有對象引用常量池中的“ABC”,也沒有其他地方引用了這個字面量,如果這時發(fā)生內(nèi)存回收,而且必要的話,“ABC”就會被清理出常量池。常量池中的其他類(接口)、方法和字段的符號引用也類似。

不過要判斷一個類是否無用就麻煩很多了。要同時滿足如下三個條件一個類才是無用的類:

  • 該類的所有實(shí)例都已經(jīng)被回收,也就是Java堆中不存在該類的任何的實(shí)例;
  • 加載該類的ClassLoader已經(jīng)被回收;
  • 該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問到該類的方法。
  • 滿足上面的三個條件,虛擬機(jī)就可以回收。不過,對于HotSpot虛擬機(jī)來說,是否回收通過-Xnoclassgc參數(shù)來設(shè)置。

    5、垃圾收集算法

    現(xiàn)在我們知道了在哪里收集垃圾以及如何判定一個對象是否是垃圾。接下來就要考慮如何收集垃圾,即垃圾收集算法。不過由于垃圾收集算法涉及到大量的程序細(xì)節(jié),所以這里僅僅介紹算法的基本思想及其發(fā)展過程。

    (1)標(biāo)記-清除算法

    標(biāo)記-清除(Mark-Sweep)算法是最基礎(chǔ)的收集算法,算法名字表明這個算法的垃圾收集過程包括兩步:標(biāo)記和清除。前面介紹的判定垃圾的過程就是標(biāo)記過程,在標(biāo)記過后的清除過程中會清理標(biāo)記為垃圾的對象。后序的垃圾收集算法都是在這個算法的基礎(chǔ)上改進(jìn)而成的。這個算法有兩個不足:一個就是標(biāo)記和清除的效率不高;第二個是空間問題,標(biāo)記清除后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多的話可能導(dǎo)致以后分配大塊內(nèi)存時失敗的問題,這樣就會觸發(fā)另一次垃圾收集操作。算法的執(zhí)行過程如下圖:


    (2)復(fù)制算法

    復(fù)制算法是為了解決標(biāo)記-清除算法效率不高的問題的,它將可用內(nèi)存按照容量分為大小相等的兩部分,每次只使用其中的一塊。當(dāng)一塊的內(nèi)存用完了,就將還存活的對象復(fù)制到另一塊,然后再把已經(jīng)使用過的內(nèi)存空間一次性清理掉。這樣使得每次是對整個半?yún)^(qū)進(jìn)行內(nèi)存回收,內(nèi)存分配時也不需要考慮內(nèi)存碎片的問題,只要移動堆頂指針,按順序進(jìn)行分配就好。算法的執(zhí)行過程如下圖:


    不過這個算法使得內(nèi)存只能一半能用,代價太高了。現(xiàn)在的虛擬機(jī)都采用這種方法來回收新生代,不過不是1:1分配的,而是將堆內(nèi)存分為以塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和一個Survivor空間。當(dāng)回收時,將Eden和Survivor中還存活的對象復(fù)制到另一塊Survivor中,然后清理Eden和使用過的Survivor空間。HotSpot虛擬機(jī)默認(rèn)的Eden和Survivor比例是8:1,即Eden占堆的80%空間,Survivor占10%的空間,每次只能使用90%的堆空間。

    不過,我們并不能保證每次回收只有不多于10%的對象存活,當(dāng)Survivor空間不夠時,需要使用其他內(nèi)存空間(老年代)進(jìn)行分配擔(dān)保,即如果Survivor空間不夠,存活的對象直接進(jìn)入老年代。

    (3)標(biāo)記-整理算法

    復(fù)制收集算法在對象存活率較高時就需要進(jìn)行較多的復(fù)制操作,效率就會降低。更關(guān)鍵的是,如果不想浪費(fèi)50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對被使用的內(nèi)存中所有對象都存活的極端情況,所以在老年代中一般不使用這種算法。

    根據(jù)老年代的特點(diǎn),可以使用另一種標(biāo)記-整理(Mark-Compact)算法,標(biāo)記過程和標(biāo)記-清除算法一樣,但后續(xù)步驟不是直接對可回收對象進(jìn)行清理,而是整理存活的對象,將存活的對象都向一端移動,然后直接清理掉邊界外的內(nèi)存。算法的執(zhí)行過程如下:


    這樣,也沒有了內(nèi)存碎片的問題。

    (4)分代收集算法

    現(xiàn)在的虛擬機(jī)都使用“分代收集”算法,這種算法只是根據(jù)對象的存活周期的不同將內(nèi)存劃分為幾塊。一般把Java堆空間分為新生代和老年代,這樣就可以根據(jù)各個年代的特點(diǎn)采用最適合的收集算法。在新生代,每次垃圾收集都會有大量的對象死去,只有少量存活,這樣就可以選擇復(fù)制算法,只需復(fù)制少量存活的對象就可以完成垃圾收集。在老年代中,對象的存活率高、沒有額外的空間對它進(jìn)行分配擔(dān)保,就必須采用標(biāo)記-清除或標(biāo)記-整理算法來進(jìn)行回收。

    6、HotSpot的算法實(shí)現(xiàn)

    前面從理論的角度介紹了對象存活判定和垃圾收集算法,接下來介紹下HotSpot虛擬機(jī)的實(shí)現(xiàn)。

    (1)枚舉根節(jié)點(diǎn)

    在對象存活判定中使用的是GC Roots可達(dá)性分析算法,可作為GC Roots的節(jié)點(diǎn)主要在全局的引用(例如常量和類靜態(tài)屬性)與執(zhí)行上下文(例如棧幀中的本地變量表)中,不過現(xiàn)在很多應(yīng)用僅僅方法區(qū)就有數(shù)百兆,如果逐個檢查這里的引用,就必然會消耗很多時間。

    另外,可達(dá)性分析對執(zhí)行時間的敏感還體現(xiàn)在GC停頓上,因為這項工作必須在一個能確保一致性的快照中進(jìn)行,就是說在整個分析過程中執(zhí)行系統(tǒng)看起來就像被凍結(jié)在某個時間點(diǎn)上,不可以出現(xiàn)分析過程中對象引用關(guān)系還在不停變化的情況,否則分析的結(jié)果就不能保證準(zhǔn)確。這是導(dǎo)致GC進(jìn)行時必須停頓所有Java執(zhí)行線程的一個重要原因,Sun將這個事件叫做“Stop the World”。

    由于目前的主流虛擬機(jī)使用的都是準(zhǔn)確式GC,所以當(dāng)執(zhí)行系統(tǒng)停頓后,并不需要檢查所有的引用和執(zhí)行上下文,虛擬機(jī)有辦法直接得知哪些地方存放著對象的引用。在HotSpot的實(shí)現(xiàn)中,使用一組OopMap的數(shù)據(jù)結(jié)構(gòu)來達(dá)到這個目的,在類加載完成的時候,HotSpot就把對象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。這樣,GC在掃描時就可以直接得知這些信息了。

    (2)安全點(diǎn)

    在OopMap的協(xié)助下,HotSpot可以快速且準(zhǔn)確的完成GC Roots的枚舉,但是,這樣可能導(dǎo)致引用關(guān)系變化,或者說OopMap內(nèi)容變化的指令非常多,如果每一條指令都生成對應(yīng)的OopMap,將會消耗大量的空間。

    實(shí)際上,HotSpot只是在特定的位置記錄OopMap的信息,這些位置稱為“安全點(diǎn)”(Safe Point),即程序執(zhí)行時并非在所有地方都能停下來開始GC,只有到達(dá)安全點(diǎn)時才能停止。SafePoint的選定不能太少也不能太多,所以,安全點(diǎn)的選定基本上是以程序“是否具有讓程序長時間執(zhí)行的特點(diǎn)”為標(biāo)準(zhǔn)進(jìn)行選定。因為每條指令執(zhí)行的時間都非常短,程序不太可能因為指令流長度太長而長時間執(zhí)行,長時間執(zhí)行最明顯的特征就是指令序列復(fù)用,比如方法調(diào)用、循環(huán)跳轉(zhuǎn)和異常跳轉(zhuǎn)等,具有這些特征的指令才會產(chǎn)生安全點(diǎn)。

    對于SafePoint來說,還有一個問題,就是如何在GC發(fā)生時讓所有的線程(不包括執(zhí)行JNI調(diào)度的線程)都跑到最近的安全點(diǎn)停下來,這里有兩種方案:搶占式中斷和主動式中斷。搶占式中斷不需要線程的執(zhí)行代碼主動去配合,在GC發(fā)生時,首先把所有的線程全部中斷,如果有線程中斷的地方不在安全點(diǎn)上,就恢復(fù)線程執(zhí)行到安全點(diǎn)。不過幾乎沒有虛擬機(jī)采用這種方法。

    另一個方法就是主動式中斷,就是說當(dāng)GC需要中斷線程的時候,不直接對線程操作,僅僅簡單的設(shè)置一個標(biāo)志,各個線程執(zhí)行時主動去輪詢這個標(biāo)志,發(fā)現(xiàn)為真時就自己中斷掛起。輪詢標(biāo)志的地方和安全點(diǎn)重合,此外還有創(chuàng)建對象需要分配內(nèi)存的地方。

    (3)安全區(qū)域

    SafePoint機(jī)制保證了程序執(zhí)行時,在不太長的時間內(nèi)就會遇到可進(jìn)入GC的SafePoint。但如果程序由于沒有分配CPU時間或者線程處于sleep或blocked狀態(tài)而沒有執(zhí)行呢?這時線程無法響應(yīng)JVM的中斷請求,無法跑到安全點(diǎn)然后掛起,JVM也不可能等著線程重新執(zhí)行。這時就需要安全區(qū)域(Safe Region)了。

    安全區(qū)域是指在一段代碼片段中,引用關(guān)系不會發(fā)生變化。在這個區(qū)域中的任何地方開始GC都是安全的,也就是說,安全區(qū)域是擴(kuò)展了的安全點(diǎn)。

    在線程執(zhí)行到安全區(qū)域中的代碼時,首先標(biāo)識自己已經(jīng)進(jìn)入安全區(qū)域,這樣,當(dāng)在這段時間JVM要發(fā)起GC時,就不用管已經(jīng)標(biāo)識進(jìn)入安全區(qū)域的線程了。在線程要離開安全區(qū)域時,要檢查系統(tǒng)是否完成了GC Roots的枚舉或者整個GC過程,如果完成了,那線程就繼續(xù)執(zhí)行,否則就等待可以安全離開安全區(qū)域的信號。

    7、HotSpot虛擬機(jī)中的垃圾收集器

    上面只是介紹了垃圾收集算法的原理,而由于HotSpot虛擬機(jī)的垃圾收集是按照分代完成的,所以虛擬機(jī)中實(shí)現(xiàn)了多個垃圾收集器。這里依據(jù)的是JDK 1.7之后的實(shí)現(xiàn),垃圾收集器如下圖所示:


    這張圖體現(xiàn)了HotSpot虛擬機(jī)中各個垃圾收集器的關(guān)系,其中上面的三個垃圾收集器工作在新生代,下面的三個收集器工作在老年代,而G1收集器在兩個部分都可以工作。收集器之間的連線表明兩個收集器可以協(xié)同工作。下面來分別介紹一下各個垃圾收集器的原理、特性和使用場景。

    (1)Serial收集器

    Serial收集器是最基本的、歷史最悠久的收集器,曾經(jīng)是JDK 1.3.1之前虛擬機(jī)的新生代收集的唯一選擇。Serial這個名字揭示了這是一個單線程的垃圾收集器,特點(diǎn)如下:

    • 僅僅使用一個線程完成垃圾收集工作;
    • 在垃圾收集時必須暫停其他所有的工作線程,知道垃圾收集結(jié)束;
    • Stop the World是在用戶不可見的情況下執(zhí)行的,會造成某些應(yīng)用響應(yīng)變慢;
    • 使用復(fù)制算法;

    Serial收集器的工作流程如下圖:


    雖然如此,Serial收集器依然是虛擬機(jī)運(yùn)行在Client模式下的默認(rèn)新生代收集器。它的優(yōu)點(diǎn)同樣明顯:簡單而高效(單個線程相比),并且由于沒有線程交互的開銷,專心做垃圾收集自然課獲得最高的單線程效率。在一般情況下,垃圾收集造成的停頓時間可以控制在幾十毫秒甚至一百多毫秒以內(nèi),還是可以接受的。

    (2)ParNew收集器

    ParNew收集器其實(shí)是Serial收集器的多線程版本,與Serial不同的地方就是在垃圾收集過程中使用多個線程,剩下的所有行為包括控制參數(shù)、收集算法、Stop the World、對象分配規(guī)則和回收策略等都一樣。ParNew收集器也使用復(fù)制算法。ParNew收集器的工作流程如下圖:


    ParNew收集器看似沒有多大的創(chuàng)新之處,但卻是許多運(yùn)行在Server模式下的虛擬機(jī)中首選的新生代收集器,因為,除了Serial收集器外,目前只有ParNew收集器能夠與CMS收集器配合工作,而CMS收集器是HotSpot在JDK 1.5時期推出的具有劃時代意義的垃圾收集器(后面會介紹到)。

    ParNew收集器在單個線程的情況下由于線程交互的開銷沒有Serial收集器的效果好。不過,隨著CPU個數(shù)的增加,它對于GC時系統(tǒng)資源的有效利用還是很有好處的。它默認(rèn)開啟的收集線程數(shù)與CPU的數(shù)量相同。可以使用-XX:ParallelGCThreads參數(shù)來限制垃圾收集的線程數(shù)。

    (3)Parallel Scavenge收集器

    Parallel Scavenge收集器和ParNew類似,是一個新生代收集器,使用復(fù)制算法,又是并行的多項成收集器。不過和ParNew不同的是,Parallel Scavenge收集器的關(guān)注點(diǎn)不同。

    CMS等收集器的關(guān)注點(diǎn)是盡可能縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目的則是達(dá)到一個可控制的吞吐量。吞吐量就是CPU用于運(yùn)行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運(yùn)行用戶代碼時間/(運(yùn)行用戶代碼時間+運(yùn)行垃圾收集時間)。如果虛擬機(jī)一共運(yùn)行100分鐘,垃圾收集運(yùn)行了1分鐘,那么吞吐量就是99%。

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

    Parallel Scavenge收集器提供了兩個參數(shù)來精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數(shù)以及直接設(shè)置吞吐量大小的-XX:GCTimeRatio參數(shù)。

    MaxGCPauseMillis參數(shù)允許的值是一個大于0的毫秒數(shù),收集器將盡可能在給定時間內(nèi)完成垃圾收集。不過垃圾收集時間的縮短是以犧牲吞吐量和新生代空間為代價的,短的垃圾收集時間會導(dǎo)致更加頻繁的垃圾收集行為,從而導(dǎo)致吞吐量的降低。

    GCTimeRatio參數(shù)的值是一個大于0且小于100的整數(shù),也就是垃圾收集時間占總時間的比率,相當(dāng)于吞吐量的倒數(shù)。如果設(shè)置為19,那允許的最大GC時間就是總時間的5%(1/(1+19))。默認(rèn)是99,也就是允許最大1%的垃圾收集時間。

    Parallel Scavenge收集器也叫吞吐量優(yōu)先收集器,它還有一個參數(shù)-XX:UseAdaptiveSizePolicy,這是一個開關(guān)參數(shù),當(dāng)這個參數(shù)打開后,就不需要手工指定新生代的大小(-Xmn)、Eden和Survivor的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細(xì)節(jié)了,虛擬機(jī)會根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動態(tài)調(diào)整這些參數(shù)以提供最適合的停頓時間或最大的吞吐量,這叫GC自適應(yīng)的調(diào)節(jié)策略。這也是Parallel Scavenge收集器和ParNew收集器的一個重要區(qū)別。

    (4)Serial Old收集器

    Serial Old是Serial的老年版本,在Serial的工作流程圖中可以看到,Serial Old收集器也是一個單線程收集器,使用“標(biāo)記-整理”算法。這個收集器主要給Client模式下的虛擬機(jī)使用。如果在Serve模式下,它有兩個用途:一個是在JDK 1.5之前的版本中與Parallel Scavenge收集器搭配使用;另一個就是作為CMS收集器的后備預(yù)案,在并發(fā)收集發(fā)生Concurrent Mode Failure時使用。這個收集器的工作流程在Serial的后半部分有所體現(xiàn)。

    (5)Parallel Old收集器

    Parallel Old收集器是Parallel Scavenge收集器的老年版本,它也使用多線程和“標(biāo)記-整理”算法。這個收集器是在JDK 1.6開始提供。

    在注重吞吐量以及CPU資源敏感的場合,都可以優(yōu)先考慮Parallel Scavenge加Parallel Old收集器的組合。Parallel Old收集器的工作流程如下:


    (6)CMS收集器

    CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標(biāo)的收集器。在重視響應(yīng)速度和用戶體驗的應(yīng)用中,CMS應(yīng)用很多。

    CMS收集器使用“標(biāo)記-清除”算法,運(yùn)作過程比較復(fù)雜,分為4個步驟:

    • 初始標(biāo)記(CMS initial mark)
    • 并發(fā)標(biāo)記(CMS Concurrent mark)
    • 重新標(biāo)記(CMS remark)
    • 并發(fā)清除(CMS Concurrent Sweep)

    其中,初始標(biāo)記和并發(fā)標(biāo)記仍然需要Stop the World、初始標(biāo)記僅僅標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對象,速度很快,并發(fā)標(biāo)記就是進(jìn)行GC RootsTracing的過程,而重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)行而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記記錄,這個階段的停頓時間一般會比初始標(biāo)記階段長,但遠(yuǎn)比并發(fā)標(biāo)記的時間短。

    由于整個過程中耗時最長的并發(fā)標(biāo)記和并發(fā)清除過程收集器線程都可以與用戶線程一起工作,所以整體上說,CMS收集器的內(nèi)存回收過程是與用戶線程一共并發(fā)執(zhí)行的。下圖是流程圖:


    CMS的優(yōu)點(diǎn)就是并發(fā)收集、低停頓,是一款優(yōu)秀的收集器。不過,CMS也有缺點(diǎn),如下:

    • CMS收集器對CPU資源非常敏感。CMS默認(rèn)啟動的回收線程數(shù)是(CPU數(shù)量+3)/4,當(dāng)CPU個數(shù)大于4時,垃圾收集線程使用不少于25%的CPU資源,當(dāng)CPU個數(shù)不足時,CMS對用戶程序的影響很大;
    • CMS收集器無法處理浮動垃圾,可能出現(xiàn)“Concurrent Mode Failure”失敗而導(dǎo)致另一次Full GC;
    • CMS使用標(biāo)記-清除算法,會產(chǎn)生內(nèi)存碎片;

    (7)G1收集器

    G1(Garbage first)收集器是最先進(jìn)的收集器之一,是面向服務(wù)端的垃圾收集器。與其他收集器相比,G1收集器有如下優(yōu)點(diǎn):

    • 并行與并發(fā):有些收集器需要停頓的過程G1仍然可以通過并發(fā)的方式讓用戶程序繼續(xù)執(zhí)行;
    • 分代收集:可以不使用其他收集器配合管理整個Java堆;
    • 空間整合:使用標(biāo)記-整理算法,不產(chǎn)生內(nèi)存碎片;
    • 可預(yù)測的停頓:G1除了降低停頓外,還能建立可預(yù)測的停頓時間模型;

    G1中也有分代的概念,不過使用G1收集器時,Java堆的內(nèi)存布局與其他收集器有很大的差別,它將整個Java堆劃分為多個大小相等的獨(dú)立區(qū)域(Region),G1收集器之所以能建立可預(yù)測的停頓時間模型,是因為它可以有計劃的避免在整個Java堆中進(jìn)行全區(qū)域的垃圾收集。G1跟蹤各個Region里垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需要的時間的經(jīng)驗值),在后臺維護(hù)一個優(yōu)先列表,每次優(yōu)先收集價值最大的那個Region。這樣就保證了在有限的時間內(nèi)盡可能提高效率。

    G1收集器的大致步驟如下:

    • 初始標(biāo)記(Initial mark)
    • 并發(fā)標(biāo)記(Concurrent mark)
    • 最終標(biāo)記(Final mark)
    • 篩選回收(Live Data Counting and Evacuation)

    收集器的流程如下圖:


    添加公眾號Machairodus,我會不時分享一些平時學(xué)到的東西~


    總結(jié)

    以上是生活随笔為你收集整理的Java垃圾收集机制的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。