Java垃圾收集器(Java GC機制)與內存分配回收策略

概述

垃圾回收(Garbage Collection,GC),顧名思義就是釋放垃圾佔用的空間,防止內存泄露。有效的使用可使用的內存,對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收。 提起java的內存回收機制,就要問三個問題java

  1. 哪些內存須要回收?
  2. 何時回收?
  3. 怎麼回收?

Java內存的動態分配和回收技術已經至關成熟。可是當咱們須要排查各類內存溢出和泄露問題時,當垃圾收集成爲系統達到更高併發量的瓶頸時,咱們就有必要學習一下垃圾回收機制。 在Java內存運行時的各個部分中,程序計數器、虛擬機棧、本地方法棧這三個區域隨線程生隨線程死。棧中的棧幀隨着方法的進入和退出有條不紊的進行着出入棧操做。這幾個區域是不須要過多的考慮內存回收的問題,由於方法或者線程結束,內存天然就跟着被回收。 然而,Java堆和方法區則不一樣——一個接口中的多個實現類須要的內存可能不同,一個方法中的多個分支須要的內存可能也不同。這部份內存的分配是動態的,咱們只有在程序運行期間才能知道會建立哪些對象。這部份內存,就是咱們關注的重點算法

回收什麼——對象死了嗎

在堆裏面存放着Java中幾乎全部的對象實例,垃圾回收器在對堆進行回收前,第一件事情就是要肯定這些對象之中哪些已經死去(不可能再被使用的對象),哪些還存活着。在回收以前咱們必須搞清楚哪些纔是「垃圾」須要咱們進行回收。數組

引用計數算法

引用計數算法(Reachability Counting)是經過在對象頭中分配一個空間做爲引用計數器來保存該對象被引用的次數(Reference Count)。每當有一個地方引用它,計數器就加一;當引用失效時,計數器就減一。而計數器爲0的對象就是沒有任何引用的「垃圾」。 客觀的說,引用計數算法的實現簡單,判斷效率高,在大部分狀況下都是一個很不錯的辦法。可是,它最大的弊端就是很難解決對象之間相互循環引用的問題:安全

public class GC {

    public Object obj;
    public static void main() {

        GC a = new GC();
        GC b = new GC();
        a.obj = b;
        b.obj = a;

        a = null;
        b = null;
    }
}
複製代碼

實際上ab這兩個對象都已經不可能再被訪問了,可是他們由於互相引用,致使計數器不爲0,因而它們永遠不會被引用計數器算法標記爲垃圾。bash

可達性算法

基於沒法解決循環引用的問題,主流的Java虛擬機裏沒有選用引用計數算法來管理內存。在Java的主流實現中,都是經過可達性算法(Reachability Analysis)來斷定對象是否存活。 在Java中:併發

  1. JAVA虛擬機棧(棧幀中的本地變量表)中的本地變量引用對象;
  2. 方法區中靜態變量引用的對象;
  3. 方法區中常量引用的對象;
  4. 本地方法棧中JNI引用的對象; 這幾種對象能夠做爲GC Roots的對象。可達性算法以GC Roots對象做爲起點,從這些節點開始搜索,其所走過的路徑成爲引用鏈,當一個對象到GC Roots沒有承認引用鏈相連時,則說明這個對象不可用,可標註爲垃圾。

tracing gc的基本思路是,以當前存活的對象集爲root,遍歷出他們(引用)關聯的全部對象(Heap中的對象),沒有遍歷到的對象即爲非存活對象,這部分對象能夠gc掉。這裏的初始存活對象集就是GC Roots。 爲何上述四種對象能夠做爲GC Roots對象可看Home3k的回答 高併發

引用(Java的四種引用)

不管是經過引用計數算法仍是可達性算法判斷對象是否存活,斷定條件都與「引用」有關。最先的Java將引用定義爲:若是reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。後來Java對引用的概念進行擴充,將引用分爲:學習

  1. 強引用(Strong Reference):指在程序中廣泛存在,相似Object obj = new Object()這類的引用,只要引用還存在,垃圾收集永遠不會回收掉被引用的對象,寧肯產生OOM也不會進行回收。
  2. 軟引用(Soft Reference): 用來描述一些還有用可是並不是必須的對象。對於軟引用關聯的對象,在系統將要發生內存溢出前,將會把這些對象列進回收範圍之中進行第二次回收。若是此次回收尚未足夠的內存,纔會拋出內存溢出。(若是一個對象的引用所有爲軟引用,GC在內存不足時就會將該對象回收——內存不足就回收)
  3. 弱引用(Weak Reference):它的強度比軟引用更弱一些,被弱引用關聯的對象祝能生存到下一次垃圾收集發生以前。當垃圾收集器開始工做,不管當前內存是否夠用,都會回收掉只被弱引用關聯的對象(一旦被GC發現,就會被回收)。
  4. 虛引用(Phantom Reference):也叫作幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來去的一個對象實例。爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知。

如何回收——垃圾收集算法

在肯定了「垃圾」是什麼——也就是哪些內存須要回收以後,垃圾回收器面臨的下一個問題就是——如何進行回收。因爲各個平臺的虛擬機操做內存的方法各不相同並且涉及大量的程序實現,這裏只介紹幾種算法的思想。spa

標記-清除算法

標記清除算法(Mark-Sweep)——首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象(標記過程就是上面講的對象斷定是否死亡)。執行過程以下圖: 線程

標記清除算法是最基礎的收集算法,後續的收集算法都是基於這種思路並對其進行改進而得的。它的不足主要有兩個: 效率問題:標記和清除過程的效率不高; 空間問題(碎片化):標記清楚以後會產生大量的不連續的內存碎片,碎片太多可能致使之後在程序在程序運行過程當中須要分配大對象時沒法找到足夠的連續內存。

複製算法

複製算法(Copying)的出現解決了標記清除算法的內存碎片問題。如今的商用虛擬機都是採用這種算法來回收新生代。它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。這樣使得每次都是對這呢個半區進行回收,即提升了回收的效率,也解決了內存碎片化的問題。複製算法的執行過程以下:

然而,這種算法的代價是講內存縮小爲原來的通常,代價高到沒法接受。幸運的是,研究代表,新生代中的對象98%都是朝生夕死的,因此並不炫耀按照一比一的比例來劃份內存空間,而是將內存分爲一塊較大的Eden區的兩個較小的Survivor區,每次使用Eden和其中的一塊Survivor。當回收時,將Eden區和Survivor中還存活的對象所有複製到另外一塊Survivor區,最後清理掉Eden區和剛纔用過的Survivor區。
若是Survivor區沒有足夠的空間存放上一次新生代收集下來的存活對象,這些對象將直接經過分配擔保機制進入老年代。 複製算法的缺點主要有:

  • 對象存活率較高時要進行較多的複製操做,效率變低。
  • 爲了應對全部對象都存活的極端狀況,須要額外的空間進行分配擔保

標記-整理算法

複製算法的缺點使得它只適用於對象存活率較低的新生代。 標記整理算法(Mark-Compact)標記過程仍然與標記清除算法同樣,但以後不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,再清理掉端邊界之外的內存區域。

分代收集

當前商業虛擬機的垃圾收集算法都採用「分代收集」算法。這種算法並無什麼新的思想,只是根據對象存活週期不停將內存劃分爲幾塊,這樣就能夠根據各個年代的特色採用最適當的收集算法。 新生代(Young Generation):在新生代中,由於大量對象的聲明週期都很短,每次回收垃圾時都有大批對象以及死去,只有少許存活,這裏的GC採用複製算法,只需付出複製少許存活對象的成本就能完成GC。這個GC機制被稱爲Minor GC或叫Young GC。 老年代(Old Generation):老年代中存放的對象存活率高,使用複製算法不只效率低下並且極度浪費內存空間。這裏的GC通常使用標記清理或者標記整理算法。這裏的GC叫作Full GC或者Major GC。   永久代(Permanent Generation):永久代中的對象生成後幾乎是永生的,回收的東西有兩種:常量池中的常量,無用的類信息。

內存分配與回收策略

對象的內存分配,往大了講就是在堆上的分配。接下來咱們學習幾條廣泛存在的內存分配規則

  • 優先在Eden區分配:大多數狀況下,最想主要在新生代Eden區中分配。當Eden區沒有足夠的控件進行分配時,虛擬機將發起一次Minor GC
  • 大對象直接進入老年代:所謂大對象,須要大量連續內存空間的Java對象,最典型的是很長的字符串以及數組(比遇到一個大對象更慘的是遇到一羣短命的大對象,這會致使內存抖動)。
  • 長期存活的對象進入老年代:虛擬機給每一個對象定義了一個年齡計數器。若是對象在Eden出生並再經理過一次Minor GC以後仍然存活並被Survivor容納的話,它的年齡會加一。對象每經歷過一次GC,年齡就加一,等增長到必定歲數(默認15),就將會被晉升到老年代。
  • 動態年齡斷定:爲了能更好的試用不一樣程序的內存情況,虛擬機並非永遠的要求對象的年齡達到閾值才能晉升老年代,若果在Survivor空間中相同年齡全部對象大小的和老是大於Survivor控件的通常,年齡大於或者等於該年齡的對象就能夠直接進入老年代。
  • 空間分配擔保:在發生Minor GC以前,虛擬機會檢查老年代最大可用的連續控件是否大於新生代全部對象總空間,若是是,那麼能夠認爲Minor GC是安全的。若是不成立,虛擬機會查看是否設置了容許失敗擔保。若是容許,就會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於,將嘗試進行一次Minor GC。若是小於,或者設置了不容許冒險,則進行一次Full GC。這裏的冒險中的風險,前面提到過新生代爲了提升內存利用率,只使用其中一個Survivor做爲輪換備份。所以當出現大量對象在Minor GC以後依然存活的狀況下,就須要老年代進行分配擔保,把Survivor沒法容納的對象直接進入到老年代。老年代要進行這樣的擔保,前提是老年代自己還有容納這些對象的空間。然而有多少對象會活下來是在內存回收完成以前是沒法預測的,因此只好取以前每一次晉升到老年代對象的平均大小做爲參考值,與老年代的剩餘空間進行比較,決定是否須要進行Full GC已變騰出更多的空間——而這顯然是存在風險的。

結語

到這裏GC的基本概念已經講完,更詳細的內容請持續關注個人博客

相關文章
相關標籤/搜索