垃圾收集器與內存分配策略

1.概述

  程序計數器,虛擬機棧,本地方法棧3個區域隨線程而生,隨線程而滅。在這幾個區域內就不須要過多考慮回收的問題,由於方法結束或者線程結束,內存天然就跟着回收了。而Java堆和方法區則不同,一個接口中的多個實現類須要的內存可能不同,一個方法中的多個分支須要的內存也可能不同,咱們只有在程序處於運行期間才能知道會建立哪些對象,這部份內存的分配和回收都是動態的,垃圾收集器所關注的也是這部份內存。java

2.對象已死嗎

  垃圾收集器在對堆進行回收前,第一件事情就是要肯定這些對象之中哪些還存活着,哪些已經死去(即不可能再被任何途徑使用的對象)。算法

2.1引用計數算法

2.1.1定義

  給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1,當引用失敗時,計數器值就減1。任什麼時候刻計數器爲0的對象就是不可能再被使用的。編程

2.1.2優點和缺陷

  優勢:實現簡單,斷定效率也很高;數組

  缺點:很難解決對象之間相互循環引用的問題,這也是主流的Java虛擬機裏面沒有選用引用計數算法來管理內存的緣由;緩存

2.1.3什麼是對象循環引用

/**
 * 對象的循環引用
 */
public class CircularReference {
    Object instance;

    public static void main(String[] args) {
        CircularReference reference1 = new CircularReference();
        CircularReference reference2 = new CircularReference();
        reference1.instance = reference2;
        reference2.instance = reference1;
    }
}

2.2可達性分析算法

2.2.1定義

  可達性分析(Reachability Analysis)經過一系列的被稱爲GC ROOTS的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC ROOTS沒有任何引用鏈相連時,則證實此對象是不可用的。安全

2.2.2可做爲GC ROOTS的對象

  • 虛擬機棧(棧楨中的本地變量表)中引用的對象;
  • 方法區中類靜態屬性引用的對象;
  • 方法區中常量引用的對象;
  • 本地方法棧中JNI(即通常所說的Native方法)引用的對象;

2.2.3可達性分析算法不可達的對象必定非死不可嗎

  真正宣告一個對象死亡,至少要經歷兩次標記過程:多線程

  若是對象在進行可達性分析後發現沒有與GC ROOTS相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲沒有必要執行併發

  若是這個對象被斷定爲有必要執行finalize()方法,對象須要在finalize()方法中從新與引用鏈上的任何一個對象創建關聯便可。若是對象沒有作此操做,那麼將對其進行第二次標記,它就真的被回收了。jvm

2.2.3.1系統會重複調用同一對象的finalize()方法嗎

  任何一個對象的finalize()方法都只會被系統自動調用一次。ide

2.2.3.2finalize()方法性能如何

  finalize()方法的運行代價高昂,不肯定性大,沒法保證各個對象的調用順序。finalize()能作的全部工做,使用try-finally或者其餘方式均可以作得更好,更及時。所以應儘可能避免使用它。

2.3引用

2.3.1定義

  在JDK1.2之前,Java中的引用的定義很傳統:若是reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。在JDK1.2以後,將引用分爲強引用(Strong Reference),軟引用(Soft Reference),弱引用(Weak Reference),虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

  強引用:是指在程序代碼之中廣泛存在的,相似Object obj = new Object()這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象;

  軟引用:描述一些還有用但非必需的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收。若是此次回收尚未足夠的內存,纔會拋出內存溢出異常;

  弱引用:描述非必需對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象;

  虛引用:它是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知;

2.3.2Java爲何擴充了引用的概念

  咱們但願能描述這樣一類對象:當內存空間還足夠時,能保留在內存之中。若是內存空間在進行垃圾收集後仍是很是緊張,則能夠拋棄這些對象。不少系統的緩存功能都符合這樣的應用場景。

2.4回收方法區

  方法區的垃圾收集主要回收兩部份內容:廢棄常量和無用的類。

2.4.1方法區如何回收廢棄常量

  回收廢棄常量與回收Java堆中的對象很是相似。以常量池中字面量的回收爲例,假如一個字符串"abc"已經進入了常量池中,可是當前系統沒有任何String對象引用常量池中的"abc"常量,也沒有其餘地方引用了這個字面量,若是這時候發生內存回收,並且必要的話,這個"abc"變量就會被系統清理出常量池。常量池中的其餘類(接口),方法,字段的符號引用也與此相似。

2.4.2方法區如何判斷一個類是不是無用的類

  • 該類全部的實例都已經被回收,也就是Java堆中不存在該類的任何實例;
  • 加載該類的ClassLoader已經被回收;
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法;

虛擬機能夠對知足上述3個條件的無用類進行回收,這裏說的僅僅是能夠,而並非和對象同樣,不使用了就必然會回收。

2.垃圾收集算法

2.1標記-清除算法

2.1.1定義

  標記-清除(Mark-Sweep)算法分爲標記清除兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象。它是最基礎的收集算法,由於後續的收集算法都是基於這種思路並對其不足進行改進而獲得的。

2.1.2執行過程

2.1.3不足

  • 效率問題:標記和清除兩個過程的效率都不高;
  • 空間問題:標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。

2.2複製算法

2.2.1定義

  複製(Copying)算法爲了解決效率問題,將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。這樣也就不用考慮內存碎片等複雜狀況。

2.2.2過程

2.2.3優缺點

優勢:實現簡單,運行高效;

缺點:將內存縮小爲原來的一半,代價有點大;

2.3標記-整理算法

2.3.1定義

  標記-整理(Mark-Compact)算法標記過程仍然與標記-清除算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉邊界之外的內存。

2.3.2過程

2.4分代收集算法

  當前商業虛擬機的垃圾收集都採用分代收集Generation Collection)算法,根據對象存活週期的不一樣將內存劃分爲幾塊。通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高,沒有額外空間對它進行分配擔保,就必須使用標記-清除標記-整理算法來進行回收。

3.垃圾收集器

  若是說收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。Java虛擬機規範中對垃圾收集器應該如何實現並無任何規定,所以不一樣的廠商,不一樣版本的虛擬機所提供的垃圾收集器均可能有很大差異。

3.1Serial收集器

  Serial收集器是最基本,發展歷史最悠久的收集器,這個收集器是一個單線程的收集器。它在進行垃圾收集時,必須暫停其餘全部的工做線程,直到它收集結束(Stop The World)。

  跟其它單線程的收集器相比,它有着簡單而高效的優勢。

3.2ParNew收集器

  ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集以外,其他行爲都與Serial收集器徹底同樣。

  ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,固然隨着可使用的CPU數量增長,它對於GC時系統的有效利用仍是頗有好處的。

  併發和並行,這兩個名詞都是併發編程中的概念,在談論垃圾收集器的上下文語境中,它們能夠解釋以下:

  • 並行(Parallel):指多條垃圾收集線程並行工做,但此時用戶線程仍處於等待狀態;
  • 併發(Concurrent):指用戶線稱與垃圾收集線程同時執行(但並不必定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行於另外一個CPU上。

3.3Parallel Scavenge收集器

  Parallel Scavenge收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器。

  CMS等收集器的關注點是儘量縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉一分鐘,那吞吐量就是99%

  停頓時間越短就越適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗,而高吞吐量則能夠高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務。

3.4Serial Old收集器

  Serial OldSerial收集器的老年代版本,它一樣是一個單線程收集器,使用標記-整理算法。它主要有兩大用途:一種用途是在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另外一種用途是做爲CMS收集器的後備方案。

3.5Parallel Old收集器

  Parallel OldParallel Scavenge收集器的老年代版本,使用多線程和標記-整理算法。

  在注重吞吐量以及CPU資源敏感的場合,均可以優先考慮Parallel ScavengeParallel Old收集器。

3.6CMS收集器

  CMSConcurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器,基於標記-清除算法。CMS收集器是HotSpot虛擬機第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工做。目前很大一部分的Java應用集中在互聯網或者B/S系統的服務端上,這類應用尤爲重視服務的響應速度,但願系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就很是符合這類應用的需求。

  CMS收集器收集過程分爲4個步驟,包括:

  • 初始標記(CMS initial mark
  • 併發標記(CMS concurrent mark
  • 從新標記(CMS remark
  • 併發清除(CMS concurrent sweep

  初始標記,從新標記這兩個步驟仍然須要Stop The World。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,併發標記階段進行進行GC Roots Tracing的過程,而從新標記階段則是爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記稍長一些,但遠比並發標記的時間短。併發清除階段會開啓用戶線程,同時GC線程開始對標記的區域作清掃。

  優勢:併發收集,低停頓;

  缺點:

  1. CMS收集器對CPU資源很是敏感;
  2. CMS收集器沒法處理浮動垃圾(Floating Garbage);
  3. CMS因爲是基於標記-清除算法實現的收集器,這意味着收集結束時會有大量空間碎片產生;

3.7G1收集器

  G1(Garbage-First)是一款面向服務端應用的垃圾收集器,主要針對配備多顆處理器及大容器內存的機器,以極高機率知足GC停頓時間要求的同時,還具有高吞吐量性能特徵。

  與其它GC收集器相比,G1具有以下特色:

  • 並行與併發(G1能充分利用CPU,多核環境下的硬件優點,使用多個CPU來縮短STW);
  • 分代收集(G1收集器可以管理整個GC堆,但保留了分代收集的概念);
  • 空間整合(G1從總體上看是基於標記-整理算法實現的,從局部上看是基於複製算法);
  • 可預測的停頓(創建可預測的停頓時間模型,便於使用者指定停頓時間);

  G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃份內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內能夠獲取儘量高的收集效率。

  G1收集器的運做大體可分爲如下幾個步驟:

  • 初始標記(Initial Marking);
  • 併發標記(Concurrent Marking);
  • 最終標記(Final Marking);
  • 篩選回收(Live Data Counting and Evacuation);

  初始標記階段僅僅是標記一下GC Roots能直接關聯到的對象,這階段須要停頓線程,但耗時很短。併發標記階段是從GC Root開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序併發執行。最終標記階段則是爲了修正在併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分標記記錄。這階段須要停頓線程,可是可並行執行。篩選回收階段首先對各個Region的回收價值和成本進行排序,根據用戶所指望的GC停頓時間來制定回收計劃。

3.8HotSpot爲何要分爲新生代和老年代

  根據對象存活週期的不一樣將內存分爲幾塊。通常將java堆分爲新生代和老年代,這樣咱們就能夠根據各個年代的特色選擇合適的垃圾收集算法。好比在新生代中,每次收集都會有大量對象死去,因此能夠選擇複製算法,只須要付出少許對象的複製成本就能夠完成每次垃圾收集。而老年代的對象存活概率是比較高的,並且沒有額外的空間對它進行分配擔保,因此咱們必須選擇標記-清除標記-整理算法進行垃圾收集。

4.內存分配與回收策略

4.1經過GC看堆空間的結構

  從垃圾回收的角度,因爲如今收集器基本都採用分代垃圾收集算法,因此Java堆還能夠細分爲:新生代和老年代。再細緻一點有:Eden空間,From Survivor(s0)To Survivor(s1)Tentired空間。

  edens0,s1區都屬於新生代,tentired區屬於老年代。在大部分狀況下,對象都會首先在Eden區域分配,在一次新生代垃圾回收後,若是對象還存活,則會進入s1("To"),而且對象的年齡還會加1(Eden->Survivor後對象的初始年齡變爲1),當它的年齡增長到必定程度(默認爲15歲),就會被晉升到老年代中。通過此次GC後,EdenFrom已經被清空。這個時候,FromTo會交換他們的角色,也就是新的To就是上次GC前的From,新的From就是上次GC前的To。無論怎樣,都會保證名爲ToSurvivor區域是空的。Minor GC會一直重複這樣的過程,直到To區被填滿,To區被填滿以後,會將全部對象移動到年老代中。

4.2對象優先在Eden分配

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

  Minor GCFull GC有什麼不同嗎?

  新生代GCMinor GC):指發生在新生代的垃圾收集動做,由於Java對象大多數都具有朝生夕滅的特性,因此Minor GC很是頻繁,通常回收速度也比較快;

  老年代GCMajor GC/Full GC):指發生在老年代的GC,出現了Major GC,常常會伴隨至少一次的Minor GC

4.3大對象直接進入老年代

  大對象是指,須要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組。

  爲了不爲大對象分配內存時因爲分配擔保機制帶來的複製而下降效率

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

  虛擬機給每一個對象定義了一個對象年齡(Age)計數器。若是對象在Eden出生並通過第一次Minor GC後仍然存活,而且能被Survivor容納的話,將被移動到Survivor空間中,而且對象年齡設爲1。對象在Survivor區中每熬過一次Minor GC,年齡就增長一歲,但它的年齡增長到必定程度(默認爲15歲),就會被晉升到老年代中。

4.5動態對象年齡斷定

  爲了能更好地適應不一樣程序的內存情況,虛擬機並非永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象能夠直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

4.6空間分配擔保

  在發生Minor GC以前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,若是這個條件成立,那麼Minor GC能夠確保是安全的。若是不成立,則虛擬機會查看HandlePromotionFailure設置值是否容許擔保失敗。若是容許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於,將嘗試着進行一次Minor GC,儘管此次Minor GC是有風險的;若是小於,或者HandlePromotionFailure設置不容許冒險,那這時也要改成進行一次Full GC

5.參考

  1. 深刻理解Java虛擬機(第2版)
  2. JavaGuide
相關文章
相關標籤/搜索