GC垃圾回收——總結

GC垃圾回收

JVM的垃圾回收機制,在內存充足的狀況下,除非你顯式調用System.gc(),不然它不會進行垃圾回收;在內存不足的狀況下,垃圾回收將自動運行html

判斷對象是否要回收的方法

引用計數算法

給對象添加一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器減1。當計數器爲0的時候,對象就能夠被回收。java

缺點:存在循環引用的狀況,致使2個循環引用對象的內存得不到釋放。目前沒有一個JVM的垃圾回收實現是使用這個算法的。程序員

主流的Java虛擬機沒有使用引用計數算法來管理內存,由於它很難解決循環引用的問題。算法

可達性分析算法

思路是:經過一系列「GC Roots」對象做爲起點,從這些節點開始向下進行搜索,搜索所走過的路徑被稱爲「引用鏈」。當一個對象到GC Roots沒有任何引用鏈相連,也就是說從GC Roots到這個對象不可達,則證實此對象是不可用的。如圖object五、object六、object7雖然互相關聯,可是他們到GC Roots是不可達的,因此他們將被判斷爲可回收的對象。數據庫

image-20191026100934969

(把一些對象當作root對象,JVM認爲root對象是不可回收的,而且root對象引用的對象也是不可回收的)數組

在Java語言中,可做爲GC Roots的對象包含下面幾種:緩存

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

即時在可達性分析法中不可達的對象,也並不是是非死不可,要真正宣告一個對象的死亡,只要要經歷2次標記的過程安全

  • 若是對象在進行可達性分析以後,發現沒有與GC Roots相連的引用鏈,那它將會被第一次標記。
  • 判斷該對象是否有必要執行finalize(),若是對象沒有覆蓋finalize方法或者finalize已經被覆蓋過了,虛擬機將這兩種狀況視爲」沒有必要執行「。服務器

    • 若是這個對象被判斷爲有必要執行finalize()方法,那麼這個對象將會被放置在一個F-Queue的隊列中,並在稍後由虛擬機創建的、低優先級的Finalizer線程去執行。這裏的」執行「指的是虛擬機會觸發這個方法,可是不承諾等待到該方法執行完畢。這樣作的緣由是:數據結構

      • 若是一個對象的finalize()方法執行緩慢,甚至發生了死循環,那麼將致使F-Queue隊列中其餘對象永久等待下去,甚至致使整個內存回收系統奔潰,由於在F-Queue中的對象沒法進行垃圾回收。
    • finalize()方法是對象最後一次逃脫死亡命運的機會,若是對象在finalize()方法中成功拯救本身——和引用鏈上任何一個對象關聯起來,好比把本身(this)賦值給某個類變量或者成員變量,那麼在第二次標記時,它將被移除」即將回收「的集合。
    • 若是對象沒有成功逃脫,那麼基本上它就真的被回收了(第二次標記)。

任何一個對象的finalize方法只會被系統自動調用一次,若是對象面臨下一次回收,它的finalize方法不會被再次執行。儘可能避免使用finalize方法,由於它只是爲了使C/C++程序員更容易接收Java所做出的一個妥協,它的運行代價高昂,不肯定性達,沒法保證各個對象的調用順序。

public class HYFinalize {


    public static void main(String[] args) {
        Book book = new Book(true);

        book.checkIn();

        // 每一本書都應該進行checkIn操做,從而釋放內存。
        // 這本書沒有進行 checkIn操做,所以,沒有執行清理操做(沒有輸出finalize execute)。也就是利用finalize方法進行終結驗證,從而找出沒有釋放對象的內存。
        new Book(true);

        // 手動調用垃圾回收
        System.gc();
    }

}

class Book {
    boolean checkOut;

    public Book(boolean checkOut) {
        this.checkOut = checkOut;
    }

    void checkIn() {
        checkOut = false;
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();

        if (checkOut) {
            System.out.println("finalize execute");
        }
    }
}

方法區中的垃圾回收

Java虛擬機規範確實說過能夠不在方法區中實現垃圾收集,方法區的垃圾收集效率也很是低,由於條件苛刻。

方法區(在HotSpot虛擬機中稱爲永久代)主要回收的內容有:廢棄常量和無用的類

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

對於無用的類則須要同時知足下面3個條件:

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

這裏解釋下爲何須要回收該類的ClassLoader?

public Class<?> getDeclaringClass() throws SecurityException {
        final Class<?> candidate = getDeclaringClass0();
  /*
  * 反射裏面使用到ClassLoader,所以要把ClassLoader幹掉,才能保證沒有地方能夠經過反射調用到Class類。
  * 而後當類的實例都會被回收了,而且該類沒有在任何地方被引用到了,那麼這個類就能夠被回收了
  */
  if (candidate != null)
  candidate.checkPackageAccess(
  ClassLoader.getClassLoader(Reflection.getCallerClass()), true);
  return candidate;
}

能夠經過虛擬機參數控制類是否被回收 -Xnoclassgc。

在大量使用反射、動態代理、GCLib等ByteCode框架、動態生成JSP 這類頻繁自定義ClassLoader的場景,都須要虛擬機具有類卸載功能,以保證永久代不會溢出。

常見的垃圾回收算法

標記-清除算法

思想

算法分爲標記、清除兩個階段:首先標記處全部須要回收的對象,在標記完成後,統一回收全部被標記的對象。

它的標記過程,使用的是可達性分析算法。

它是最基礎的算法,由於後面的垃圾回收算法都是基於標記-清除算法進行改進。標記-清除也是最簡單的算法。

image-20191026104917493

優勢

實現簡單

缺點

  • 一個是效率問題,標記和清除過程,兩個效率都不高
  • 另一個是空間問題,標記-清除以後,會產生大量不連續的內存碎片。空間內存碎片太多,那麼須要給較大的對象分配內存空間的時候,沒法找到足夠內存空間,而不得不提早觸發一次垃圾回收。

複製收集算法

思想

將可用內存劃分爲大小相等的兩塊,每次只使用其中一塊。當這一塊內存用完了,就將還活着的對象複製到另一塊上,而後再把已使用過的內存空間一次性清理掉。

優勢

這樣每次都是對整個半區進行回收,內存分配時也不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。

缺點

只是這種算法的代價將內存縮小爲原來的一半,代價過高了。

image-20191026105459849

如今商業虛擬機都採用這種方法來回收新生代,IBM公司研究代表,新生代中的對象98%都是朝生暮死的,因此並不須要按照1:1來劃份內存空間,而是將內存劃分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。

當回收時,將Eden和Survivor中還存活的對象一次性複製到另一個Survivor空間上,最後清理Eden和剛纔使用過的Survivor空間。

HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是每次新生代中可用內存爲整個新生代的90%,只有10%的內存會被浪費。

固然,98%的對象可回收只是通常場景下的數據,咱們沒有辦法保證每次回收都只有很少於10%內存大小的對象存活,當Survivor空間不夠時,須要依賴其餘內存進行分配擔保

分配擔保:若是另一塊Survivor空間沒有足夠空間存放上一次新生代回收存活下來的對象時,這些對象將直接進入老年代。

Mark-Compact(標記-整理算法)

思想

複製收集算法在對象存活率較高時,就要進行較多的複製操做,致使效率變低。因爲老年代存活率較高,因此通常不採用這種算法。

根據老年代的特色,有人提出了」標記-整理「算法,標記過程仍然使用」可達性分析算法「,而後讓全部的存活對象向一端移動,而後直接清理掉端邊界之外的內存。

image-20191026112718120

優勢

  • 不容易產生內存碎片
  • 內存利用率高

缺點

  • 存活對象多而且分散的時候,移動次數多,效率低下
  • 程序暫停

分代收集算法

思想:

只是根據對象的存活週期的不一樣把堆分紅新生代和老年代(永久代指的是方法區),這樣就能夠根據各個年代的特色採用最適當的收集算法。

「分代收集」是目前大部分JVM的垃圾收集器所採用的算法。

在新生代中,每次垃圾收集都有大量對象死去,只有少了存活,那就採用複製算法,只需付出少許對象的複製成本就能夠完成收集。

而老年代中對象存活率高,而且沒有其餘空間對它進行分配擔保,就必須使用標記-清理 或者 標記-整理算法進行回收。

新生代
  • 在新生代裏面存放的是存活時間比較短的對象,如某一個方法的局域變量、循環內的臨時變量等等
  • 在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。
  • 新生代裏面分紅一份較大的Eden空間和兩份較小的Survivor(存活)空間。每次只使用Eden和其中一塊Survivor空間,而後垃圾回收的時候,把存活對象放到未使用的Survivor空間中,清空Eden和剛纔使用過的Survivor空間。
  • 一塊Eden和一塊Survivor區,比值爲8:1。這樣子的設置是有緣由的。新生代採用複製算法,若是單純的把內存分爲2塊,因爲存活對象不多,那麼存放存活對象的那塊堆內存,會有不少內存浪費。所以,使用兩塊10%的內存做爲空閒和活動區間(兩塊Survivor區),而另外80%的內存(Eden區),則是用來給新建對象分配內存的。一旦發生GC,將10%與另外80%的活動區間 中存活的對象轉移到10%的空閒區間,接下來,將以前90%的內存所有釋放。
  • 絕大多數剛建立的對象會被分配在Eden區,其中的大多數對象很快就會消亡。Eden區是連續的內存空間,所以在其上分配內存極快

20160730141640502

新生代垃圾回收流程

  • 當Eden區滿的時候,執行Minor GC,將消亡的對象清理掉,並將剩餘的對象複製到一個存活區Survivor0(此時,Survivor1是空白的,兩個Survivor總有一個是空白的)
  • 此後,每次Eden區滿了,就執行一次Minor GC,並將Eden剩餘的存活對象都添加到Survivor0
  • 當Survivor0也滿的時候,將其中仍然活着的對象直接複製到Survivor1,而後清理掉Survivor0區。以後Eden區執行Minor GC後,就將剩餘的對象添加Survivor1(此時,Survivor0是空白的)。重複上述步驟,只不過此次是Eden區和Survivor1區配合。

Eden區是連續的空間,且Survivor總有一個爲空。通過一次GC和複製,一個Survivor中保存着當前還活 着的對象,而Eden區和另外一個Survivor區的內容都再也不須要了,能夠直接清空,到下一次GC時,兩個Survivor的角色再互換。所以,這種方 式分配內存和清理內存的效率都極高,這種垃圾回收的方式就是著名的「中止-複製(Stop-and-copy)」法),這不表明着中止複製清理法很高效,其實,它也只在這種狀況下高效,若是在老年代採用中止複製,則挺悲劇的。

老年代(tenured)
  • 存放的是存活時間比較長的對象,如緩存對象、數據庫鏈接對象、單例對象等等
  • 老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就只能使用「標記-清除」或「標記-整理」算法來進行回收。
  • 在新生代裏的每個對象,都會有一個年齡,當這些對象的年齡到達必定程度時(年齡就是熬過的GC次數,每次GC若是對象存活下來,則年齡加1),則會被轉到年老代,而這個轉入年老代的年齡值,通常在JVM中是能夠設置的。
永久代
  • 在堆區外有一個永久代,
  • 對永久代的回收主要是無效的類和常量,而且回收方法同老年代

HotSpot的GC算法實現

枚舉根節點

可做爲GC Roots的節點主要在全局性的引用(例如常量或者靜態屬性)、執行上下文中(例如棧幀中的本地變量表),如今不少應用僅僅方法區就有數百兆,若是要逐個檢查這裏面的引用,找出GC Roots節點,那麼必然會消耗不少的時間,

另外,可達性分析對執行時間的敏感還提如今GC停頓上,由於這項分析工做必須在一個能確保一致性的快照中進行——這裏」一致性「的意思是整個系統看起來像被凍結在某個時間點上,不能夠出現分析過程當中引用關係還在變化的狀況,該點不知足的話,分析結果的準確性將沒法保證。這點是致使GC進行時必須停頓全部Java執行線程的其中一個重要緣由。即時是在號稱幾乎不會停頓的CMS收集器中,枚舉根節點時也是要停頓的。

準確式內存管理

準確式內存管理,又稱爲「準確式GC」。

虛擬機能夠知道內存中某個位置的數據具體是什麼類型。好比內存中有一個32位的整數123456,它究竟是一個引用類型,指向123456的地址,仍是一個數值爲123456的整數,虛擬機將由能力辨別出來,這樣子才能在GC的時候,準確判斷堆上的數據是否還可能被使用。

因爲使用準確式內存管理,Exact VM拋棄了基於handler的對象查找方式(緣由是GC後對象可能被移動位置,好比對象的地址本來爲123456,而後該對象被移動到654321的地址,在沒有明確信息代表內存中的哪些數據是引用的前提下,虛擬機是不敢把內存中全部123456的值改成654321的,由於不知道這個值是整數仍是指向另一塊內存的地址,所以有些虛擬機使用句柄來保持引用的穩定),經過準確式內存管理,能快速判斷該數據是否引用,就能夠避免使用句柄,從而減小一次查找地址的開銷,提升執行性能。

因爲目前主流的Java虛擬機都是採用準確式GC,因此當執行系統停頓下來後,並不須要一個不漏的檢查完執行上下文和全局的引用位置,虛擬機應當有辦法直接指導哪些地方存放着對象引用。

HotSpot怎麼快速找到可達對象

在HotSpot實現中,使用一組稱爲OopMap的數據結構來達到這個目的。

在類加載完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程當中,也會在特定位置記錄下棧和寄存器中哪些位置是引用。這樣子,在GC掃描的時候,就能夠直接知道哪些是可達對象了。

安全點

在OopMap的協助下,HotSpot能夠快速準確完成GC Roots枚舉。可能致使OopMap內容變化的指令很是多,若是爲每一條指令都生成對應的OopMap,那麼將會須要大量的額外空間,這樣GC的成本將會變得很高。

實際上,HotSpot也沒有爲全部指令生成OopMap,只有在特定位置生成這些信息,這個位置稱爲「安全點」。

程序在執行過程當中,並不是在全部地方均可以停頓下來進行GC,只有在到達安全點時才能暫停。安全點的選定既不能太以致於讓GC等待太長的時間,也不能過多以致於增大運行時的負荷。因此安全點的選定是以「是否讓程序長時間運行」爲標準進行選定的。長時間運行最明顯的特徵是指令複用,好比說方法調用、循環跳轉、異常跳轉等,因此具備這些功能的指令纔會產生安全點。

對於安全點,另一個須要考慮的問題,是如何讓全部線程跑到最近的安全點再停頓下來,這裏有2種方案可供選擇:搶佔式中斷、主動式中斷。

搶佔式中斷

搶佔式中斷不須要線程的執行代碼主動去配合。

在GC發生時,首先把全部線程中斷,若是發現有線程中斷的地方不在安全點上,就恢復線程,讓它跑到安全點上。

如今幾乎沒有虛擬機採用搶佔式中斷來暫停線程。我的以爲是太粗暴了,好比直接中斷線程。

主動式中斷

主動中斷的思想是:當GC須要中斷線程時,不直接對線程操做,僅僅設置一個標誌,各個線程執行時主動去輪詢這個標誌,當發現中斷標記爲真就本身中斷掛起。輪詢標記的位置就是安全點的位置。

安全區域

使用安全點是否已經完美解決何時進入GC的問題。可是假如程序不執行呢?因此的程序不執行就是沒有分配CPU時間片,最典型的就是線程處於sleep或者阻塞狀態,這時候線程沒法執行到安全點,而且響應中斷掛起。JVM也不太可能等待線程從新得到CPU時間片,這時候就須要安全區域來解決。

安全區域指在一段代碼片斷中,引用關係不會發生變化。這個區域任務地方開始GC都是安全的。咱們能夠把安全區域看作是擴展的安全點。

在線程執行到安全區域時,首先標識本身已經進入安全區域了,那樣,當這段時間內發生GC時,就不用管那些標識爲安全區域狀態的線程了。

在線程要離開安全區域時,它首先檢查系統是否已經完成根節點枚舉,若是完成,線程就繼續執行,不然,它就繼續等待直到收到能夠離開安全區域的信號。

垃圾收集器

若是說垃圾收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。

image-20191026130140758

上圖展現了不一樣分代的垃圾收集器,若是兩個收集器之間存在連線,那麼說明它們能夠搭配使用。

垃圾收集器所處的區域,則代表它是新生代收集器,仍是老年代收集器。

Serial收集器

Serial收集器是最基本,發展歷史最悠久的收集器。Serial是一個單線程收集器。是新生代收集器。

Serial收集器在進行垃圾收集的時候,必須暫停其餘全部的線程,直到它收集結束。「Stop the World」暫停線程 這個工做是後臺自動發起和完成的,在用戶不可見的狀況下把用戶正常工做的線程停掉,這對於不少應用來講是很難接受的。假如你的計算機每運行1個小時就要停頓5分鐘,你會有怎樣的心情?下圖展現了Serial收集器的運行過程:

image-20191026131046698

「Stop the World」是沒有辦法避免的,舉個簡單例子:你媽媽在打掃房間的時候,你還一遍扔垃圾,這怎麼打掃的完?。目前之間儘可能減小停頓線程的時間。

serial收集器仍然是虛擬機運行在client模式下的默認新生代垃圾收集器。它也有因爲其餘收集器的地方:簡單而高效。對於單CPU的環境來講,Serial收集器因爲線程交互的開銷,專心作垃圾收集,天然能夠得到最好的單線程收集效率。在用戶的桌面應用場景中,分配給虛擬機管理的內存通常不會很大,收集幾十兆甚至一兩百兆的新生代,停頓時間能夠控制在幾十毫秒甚至一百毫秒之內,只要不是頻繁發生,仍是能夠接受的。

ParNew收集器

ParNew收集器其實就是Serial收集器的多線程版本。除了多線程進行垃圾收集以外,其餘都和Serial同樣。

是新生代收集器。

ParNew收集器的工做過程如圖:

image-20191026135929901

ParNew收集器是許多運行在Server模式下的虛擬機首選的新生代收集器。由於除了Serial收集器外,只有它能和CMS收集器配合工做。

Parallel Scavenge收集器

Parallel Scavenge是新生代收集器。它也是使用複製算法的收集器,又是並行的多線程收集器。看上去了ParNew同樣,那麼它有什麼特別之處呢?

Parallel Scavenge是爲了達到一個可控制的吞吐量。吞吐量=運行用戶代碼的時間 / (運行用戶代碼的時間 + 垃圾收集的時間)。高吞吐量代表CPU時間被有效的利用,儘快完成程序的運算任務。

Parallel Scavenge收集器提供了參數控制最大垃圾收集停頓時間,虛擬機將盡量保證垃圾回收的時間不超過該值。不過你們不要任務把這個參數的值設小一點就可使垃圾收集速度加快,GC停頓時間縮短,是以犧牲吞吐量和新生代空間來換取的,系統會把新生代調小一些,收集300MB的新生代確定比收集500MB的快,但這也致使垃圾收集更頻繁一些。原來10秒收集一次,每次停頓100毫秒,如今變成5秒收集一次,每次停頓70毫秒。停頓時間是降低了,可是系統吞吐量下來了。

因爲和吞吐量關係密切,Parallel Scavenge也被稱爲吞吐量優先收集器

Parallel Scavenge還有一個參數,這個參數打開之後,就不須要手工指定新生代大小、Eden和Survivor比例等參數,虛擬機會根據運行狀況,動態調整這些參數,已提供最適合的停頓時間,這種調節方式成爲GC自適應調節策略。自適應調節策略也是Parallel Scavenge收集器和ParNew收集器的一個重要區別。

Parallel Scavenge沒法和CMS配合工做。

Serial Old收集器

Serial Old是Serial收集器的老年代版本。它一樣是一個單線程收集器,使用標記-整理算法。這個收集器的主要意義是給Client模式下的虛擬機器使用,工做過程以下:

image-20191026142719613

Parallel Old收集器

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

這個收集器是JDK1.6以後纔開始提供的,在此以前,Parallel Scavenge收集器一直處於比較尷尬的位置,由於若是新生代選擇了Parallel Scanvenge收集器,老年代除了Serial Old收集器以外別無選擇。老年代Serial Old收集器在服務端的拖累,使用Parallel Scavenge收集器未必能在總體應用上得到吞吐量最大化的效果。因爲單線程的老年代收集,沒法充分利用服務端多CPU的能力,在老年代很大並且硬件比較高級的環境,這種組合的吞吐量甚至還不如ParNew + CMS組合給力。

直到Parallel Old收集器出現後,Parallel Scavenge纔有了比較名副其實的應用組合。在注重吞吐量與多CPU的場景,能夠優先考慮Parallel Scavenge 和 Parallel Old收集器。Parall Old工做狀態如圖:

image-20191026143854952

CMS收集器

概念

CMS是一種以獲取最短停頓時間爲目標的收集器。互聯網應用就很是注重服務器的響應速度,但願系統停頓時間最短,已給用戶帶來最好的體驗。CMS收集器就很是符合這類應用的需求。

CMS收集器基於標記-清除算法實現的。它的運做過程分爲4個步驟:初始標記、併發標記、從新標記、併發清除

其中,初始標記、從新標記兩個步驟仍然須要暫停用戶線程。

  • 初始標記僅僅是標記一下GC Roots可以直接關聯的對象,速度很快。
  • 併發標記就是進行GC Roots 向下查找過程,也就是從GC Roots開始,對堆中對象進行可達性分析。這時候用戶線程還能夠繼續執行。
  • 從新標記階段是爲了修正併發標記期間因用戶線程繼續運做而致使標記產生變更的那一部分對象標記記錄

    • 這個階段的標記時間通常比初始標記稍長一點,但遠比並發標記時間短。
  • 併發清除是GC垃圾收集線程 和 用戶線程並行的,清理被回收的對象。

因爲整個過程當中耗時最長的併發標記併發清除的階段收集器均可以和用戶線程並行工做,因此整體上來講,CMS收集器的內存回收是與用戶線程一塊兒併發執行的。

image-20191026145340936

優勢

減小了GC停頓時間

缺點

  • 對CPU資源敏感,在併發階段,由於佔用一部分CPU資源,所以會致使程序變慢。當CPU個數比較少的時候,對用戶影響可能很大。

    • 爲了因對這種狀況,虛擬機提供了一種增量式併發收集器,是CMS收集器的變種。在併發標記、併發清除階段,讓GC線程、用戶線程交替運行,儘可能減小GC線程獨佔資源的時間,這樣一來,整個垃圾收集的時間更長,但對用戶的影響就少一些。實踐證實,增量式併發收集器的效果很通常,已經不提倡用戶使用了。
  • 沒法處理浮動垃圾。在併發清除階段,用戶線程還在運行着,還會產生新的垃圾。這一部分垃圾出如今標記過程以後,CMS沒法在當次收集中處理掉它們,只好留到下一次GC的時候再清理掉。這一部分垃圾就稱爲浮動垃圾

    • 因爲垃圾收集階段,用戶線程還在運行,所以須要預留足夠的內存空間給用戶使用。 所以CMS收集器不能像其餘收集器同樣,等到老年代幾乎被填滿了再進行收集,須要預留一部份內存空間提供用戶線程使用。在JDK1.6中,CMS的啓動閾值爲92%,也就是老年代使用了92%以後,CMS收集器就會進行垃圾回收。
    • 若是CMS運行期間預留的內存不夠用戶線程使用,就會出現一次「Concurent Mode Fail」失敗,這時虛擬機臨時啓用Serial Old收集器來從新進行老年代的垃圾回收,這樣的停頓時間就長了。所以,若是啓動閾值設置得過高,容易致使「Concurrent Mode Fail」,性能反而下降。
  • CMS是一塊基於標記-清除實現的垃圾收集器,那麼在收集結束時會有大量內存碎片產生。當內存碎片過多的時候,若是要對大對象進行內存分配,可是沒法找到足夠大的連續內存空間進行分配,就會觸發一次Full GC。

    • 爲了解決這個問題,CMS提供一個開關,默認是開啓的,表示CMS要進行Full GC的時候,開啓內存碎片的整理合並過程,並該過程是沒法併發的,所以停頓時間就變長了。

G1收集器

G1收集器是當前收集器發展最前沿的成果之一。G1收集器是一款面向服務端應用的垃圾收集器。Hotspot團隊但願G1收集器將來能替換掉CMS收集器。

在G1以前的其餘收集器進行收集的範圍都是整個新生代或者老年代,而G1再也不是這樣。G1將堆分紅許多大小相同的區域單元,每一個單元稱爲Region。Region是一塊地址連續的內存空間,G1模塊的組成以下圖所示:

image-20191026170627820

G1收集器將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔離的了,它們都是一部分Region(不須要連續)的集合。Region的大小是一致的,數值是在1M到32M字節之間的一個2的冪值數,JVM會盡可能劃分2048個左右、同等大小的Region。

G1收集器具有以下特色

  • 併發與並行:G1能充分利用CPU、多核環境的硬件優點。使用多個CPU縮短Stop the world,也就是工做線程的暫停時間。在執行GC動做的同時,G1收集器仍然可以經過併發的方式讓Java程序執行。
  • 分代收集:與其餘收集器同樣,分代概念在G1收集器中仍然得以保留。雖然G1收集器不用其餘收集器配合技能管理整個GC堆,但他依然可以採用不一樣方式去處理新建立的對象和已經存活了一段時間、熬過屢次GC的舊對象 以取得更好的收集效果。
  • 空間整合:G1總體上看是基於標記-整理算法實現的收集器,從局部看(兩個Region之間),是基於複製算法實現的。使用這兩種算法,在G1運行期間不會產生內存碎片,垃圾收集後,能提供規整的可用內存。這種特性有利於程序長時間運行,分配大對象時不會由於沒法找到連續內存空間而提早觸發下一次GC。
  • 可預測的停頓:G1除了追求減低停頓之外,還創建了可預測的停頓時間模型。能讓使用者明確指定在一個長度爲M毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過N毫秒。

停頓時間模型

G1收集器之因此能創建可預測的停頓時間模型,是由於它能夠有計劃的避免對整個堆進行垃圾收集。G1維護了一份優先列表,每次根據容許的收集時間,優先回收價值最大的region。(怎樣纔是價值大?region採用複製算法,那麼若是一個region中垃圾不少,存活對象不多,那麼這個遷移存活對象的工做就不多,而且收集完以後,可以獲得的內存空間不少,這種就是價值大的region)。

這種使用region劃份內存空間,而且有優先級的區域回收方式,保證G1收集器在有限的時間內,能夠獲取儘量高的效率。

避免全堆掃描

一個對象分配在某個Region中,可是它能夠與整個Java堆中任意對象發生引用關係。那麼作可達性分析法判斷對象是否存活的時候,豈不是掃碼怎麼java堆才能確保準確性?這個問題其實並不是G1纔有,只是G1更加突出而已。若是回收新生代不得不掃描老年代的話,那麼Minor GC的 效率可能降低很多。

在G1收集器中,Region之間的引用 和 其餘收集器中新生代和老年代之間的引用,虛擬機都是使用Remembered Set來避免全堆掃描。在G1中,每個Region都有一個Remembered Set。當對引用進行寫操做的時候,G1檢查該引用的對象是否在別的region中,是的話,則經過CardTable把相關引用信息存到被引用對象的Remembered Set中。當進行內存回收時,把RememberSet加入到GC Roots根節點的枚舉範圍。這樣就能夠保證不全堆掃描也不會有遺漏。

工做流程

若是不計算Remembered Set的操做,G1收集器的運做大體分爲以下操做:初始標記、併發標記、最終標記、篩選回收。

  • 初始標記階段:僅僅只是標記一下GC Roots直接關聯的對象。而且修改TMAS(Next Top at Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確可用的Region中建立對象。這階段須要停頓線程,但耗時很短。
  • 併發標記階段:從GC Roots開始,對堆中對象進行可達性分析,找出存活對象。這階段耗時較長,但可與用戶程序併發執行
  • 最終標記階段:修復在併發標記階段因用戶程序運行致使標記發生變化的那一步部分標記記錄。虛擬機將這段時間對象變化記錄到Remembered Set Log中,在最終標記階段把Remembered Set Log合併到Rmembered Set中。這階段須要停頓線程。
  • 篩選回收階段:首先對各個Region的回收價值和成本進行排序,根據用戶所指望的GC停頓時間來指定回收計劃。這個階段其實也能夠作到和用戶線程一塊兒併發執行,但由於只回收一部分Region,時間是用戶可控制的,並且停頓線程能大幅提升收集效率。所以沒有實現爲和用戶線程併發執行。

image-20191026182758933

內存分配與回收策略

對象的分配,主要在新生代的Eden區上,若是啓動了本地線程分配緩存,那麼將優先在TLAB上分配。少數狀況下,也可能直接分配在老年代中。

對象優先在Eden區分配

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

大對象直接進入老年代

大對象是指須要大量連續內存空間的對象。最典型的就是很長的數組或者字符串。常常出現大對象,就容易致使內存還有很多空間的時候,就觸發GC來獲取足夠的連續內存空間來放置這些大對象。

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

虛擬機怎麼識別哪些對象應該存放到新生代,哪些對象應該存放到老年代?爲了作到這點,虛擬機給每一個對象定義了一個對象年齡計數器(放在對象頭的Mark World中)。

若是對象在Eden區出生,經歷了第一次Minor GC後仍然存活,而且能被Survivor容納的話,將被移動到Survivor空間,而後年齡計數器設置爲1。

對象在Survivor中每熬過一次Minor GC,年齡就增長1歲。當它的年齡到達必定程度(默認是15歲),就會被移動到老年代。

動態年齡判斷

若是Survivor中相同年齡的對象大小總和 大於 Survivor空間的一半,年齡大於或等於該年齡的對象就能夠進入老年代。無需等到閾值(好比15歲)。

空間分配擔保

新生代使用複製算法,可是爲了保證內存利用率,所以只用其中一塊Survivor區保存存活對象。所以當發生大量對象在Minor GC後仍然存活的狀況,就須要老年代進行分配擔保,就是指把Survivor沒法容納的對象直接進入老年代。老年代要進行這樣的擔保,前提是老年代自己還有容納這些對象的剩餘空間,一共有多少對象可以存活下來,在實際內存回收完成以前是沒法知道的,只好取以前成功晉升老年代的對象容量大小的平均值,與老年代剩餘空間進行比較。決定是否進行Full GC以騰出更多的空間。

具體操做爲:

在進行Minor GC以前,虛擬機將會檢查老年代最大可用的連續空間是否大於新生代全部對象總空間。若是這個條件成立,則認爲Minor GC是安全的(老年代能夠擔保成功)。

若是不成立,則虛擬機會查看HandlePromotionFailure設置值是否容許擔保失敗。若是容許,將檢查老年代最大可用連續空間是否 大於 以前成功晉升老年代的對象容量大小的平均值。若是大於,則嘗試進行一次Minor GC。若是小於,或者HandlePromotionFailure設置爲不容許擔保失敗,則進行一次Full GC。

HandlePromotionFailure通常打開,避免頻繁Full GC。

在JDK6以後,已經再也不使用HandlePromotionFailure這個參數了,JDK6以後的規則變爲:

  • 只要老年代的連續內存空間 大於新生代對象總大小 或者 大於歷次晉升的對象平均大小,就進行minor GC,不然進行Full GC。

幾種不一樣的垃圾回收類型

Minor GC

Minor GC又稱爲新生代GC。指發生在新生代的垃圾收集動做。由於Java對象大多很快死亡,因此Minor GC很是頻繁,通常回收速度也比較快。

Major GC/Full GC

又稱爲老年代GC。指發生在老年代的GC。出現了Major GC,常常伴隨着一次Minor GC。Major GC通常速度比Minor GC慢10倍以上。

其餘內存

除了Java堆和永久代以外,還有一些區域會佔用比較多的內存,這裏全部內存總和受到操做系統進程最大內存的限制。

  • Direct Memory:可用過-XX: MaxDirectMemorySize調整大小,內存不足時,會拋出OutOfMemoryError 或者 OutOfMemoryError : Direct buffer memory
  • 線程堆棧:可經過-Xss調整大小。內存不足時拋出StackOverflowError(即沒法分配新的棧幀)或者OutOfMemoryError: unable to create new native thread(沒法創建新的線程)
  • Socket緩衝區:每一個Socket鏈接都有Send和Receive兩個緩衝區,分別佔大約37KB和25KB內存,鏈接多的話,這兩塊內存佔用也比較可觀。若是沒法分配,則可能拋出 IOException : Too many open files異常。
  • JNI代碼:若是代碼中使用JNI調用本地庫,那本地庫使用的內存也不在堆中
  • 虛擬機和GC:虛擬機、GC的代碼執行也要消耗必定的內存

問題

爲何要劃分紅年輕代和老年代?

爲了針對不一樣的內存區域採用不一樣垃圾收集算法,從而提升效率

年輕代爲何被劃分紅eden、survivor區域?

經過劃分eden、survivor區,可以提升年輕代的內存使用率。由於年輕代的大部分對象都會很快死去,所以只須要使用少部分的內存來保留存活對象。

參考

https://blog.csdn.net/mccand1...

重點: https://www.cnblogs.com/aspir...、https://www.cnblogs.com/aspirant/category/1195271.html

https://www.cnblogs.com/1024Community/p/honery.html#25-%E6%96%B9%E6%B3%95%E5%8C%BA%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E9%9C%80%E8%A6%81%E5%9B%9E%E6%94%B6

[https://www.cnblogs.com/heyon...

相關文章
相關標籤/搜索