天天看點

java gc回收的是堆記憶體嗎,Java垃圾收集器(Java GC機制)與記憶體配置設定回收政策

概述

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

哪些記憶體需要回收?

什麼時候回收?

怎麼回收?

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;

}

}

複制代碼

實際上a和b這兩個對象都已經不可能再被通路了,但是他們因為互相引用,導緻計數器不為0,于是它們永遠不會被引用計數器算法标記為垃圾。

可達性算法

基于無法解決循環引用的問題,主流的Java虛拟機裡沒有選用引用計數算法來管理記憶體。在Java的主流實作中,都是通過可達性算法(Reachability Analysis)來判定對象是否存活。 在Java中:

JAVA虛拟機棧(棧幀中的本地變量表)中的本地變量引用對象;

方法區中靜态變量引用的對象;

方法區中常量引用的對象;

本地方法棧中JNI引用的對象; 這幾種對象可以作為GC Roots的對象。可達性算法以GC Roots對象作為起點,從這些節點開始搜尋,其所走過的路徑成為引用鍊,當一個對象到GC Roots沒有認可引用鍊相連時,則說明這個對象不可用,可标注為垃圾。

java gc回收的是堆記憶體嗎,Java垃圾收集器(Java GC機制)與記憶體配置設定回收政策

tracing gc的基本思路是,以目前存活的對象集為root,周遊出他們(引用)關聯的所有對象(Heap中的對象),沒有周遊到的對象即為非存活對象,這部分對象可以gc掉。這裡的初始存活對象集就是GC Roots。 為什麼上述四種對象可以作為GC Roots對象可看Home3k的回答

引用(Java的四種引用)

無論是通過引用計數算法還是可達性算法判斷對象是否存活,判定條件都與“引用”有關。最早的Java将引用定義為:如果reference類型的資料中存儲的數值代表的是另外一塊記憶體的起始位址,就稱這塊記憶體代表着一個引用。後來Java對引用的概念進行擴充,将引用分為:

強引用(Strong Reference):指在程式中普遍存在,類似Object obj = new Object()這類的引用,隻要引用還存在,垃圾收集永遠不會回收掉被引用的對象,甯可産生OOM也不會進行回收。

軟引用(Soft Reference): 用來描述一些還有用但是并非必須的對象。對于軟引用關聯的對象,在系統将要發生記憶體溢出前,将會把這些對象列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的記憶體,才會抛出記憶體溢出。(如果一個對象的引用全部為軟引用,GC在記憶體不足時就會将該對象回收——記憶體不足就回收)

弱引用(Weak Reference):它的強度比軟引用更弱一些,被弱引用關聯的對象祝能生存到下一次垃圾收集發生之前。當垃圾收集器開始工作,無論目前記憶體是否夠用,都會回收掉隻被弱引用關聯的對象(一旦被GC發現,就會被回收)。

虛引用(Phantom Reference):也叫做幽靈引用或者幻影引用,它是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來去的一個對象執行個體。為一個對象設定虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。

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

在确定了“垃圾”是什麼——也就是哪些記憶體需要回收之後,垃圾回收器面臨的下一個問題就是——如何進行回收。由于各個平台的虛拟機操作記憶體的方法各不相同而且涉及大量的程式實作,這裡隻介紹幾種算法的思想。

标記-清除算法

标記清除算法(Mark-Sweep)——首先标記出所有需要回收的對象,在标記完成後統一回收所有被标記的對象(标記過程就是上面講的對象判定是否死亡)。執行過程如下圖:

java gc回收的是堆記憶體嗎,Java垃圾收集器(Java GC機制)與記憶體配置設定回收政策

标記清除算法是最基礎的收集算法,後續的收集算法都是基于這種思路并對其進行改進而得的。它的不足主要有兩個: 效率問題:标記和清除過程的效率不高; 空間問題(碎片化):标記清楚之後會産生大量的不連續的記憶體碎片,碎片太多可能導緻以後在程式在程式運作過程中需要配置設定大對象時無法找到足夠的連續記憶體。

複制算法

複制算法(Copying)的出現解決了标記清除算法的記憶體碎片問題。現在的商用虛拟機都是采用這種算法來回收新生代。它将可用記憶體按容量劃分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊的記憶體用完了,就将還存活着的對象複制到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對這呢個半區進行回收,即提高了回收的效率,也解決了記憶體碎片化的問題。複制算法的執行過程如下:

java gc回收的是堆記憶體嗎,Java垃圾收集器(Java GC機制)與記憶體配置設定回收政策

然而,這種算法的代價是講記憶體縮小為原來的一般,代價高到無法接受。幸運的是,研究表明,新生代中的對象98%都是朝生夕死的,是以并不炫耀按照一比一的比例來劃分記憶體空間,而是将記憶體分為一塊較大的Eden區的兩個較小的Survivor區,每次使用Eden和其中的一塊Survivor。當回收時,将Eden區和Survivor中還存活的對象全部複制到另一塊Survivor區,最後清理掉Eden區和剛才用過的Survivor區。

java gc回收的是堆記憶體嗎,Java垃圾收集器(Java GC機制)與記憶體配置設定回收政策

如果Survivor區沒有足夠的空間存放上一次新生代收集下來的存活對象,這些對象将直接通過配置設定擔保機制進入老年代。 複制算法的缺點主要有:

對象存活率較高時要進行較多的複制操作,效率變低。

為了應對所有對象都存活的極端情況,需要額外的空間進行配置設定擔保

标記-整理算法

複制算法的缺點使得它隻适用于對象存活率較低的新生代。 标記整理算法(Mark-Compact)标記過程仍然與标記清除算法一樣,但之後不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,再清理掉端邊界以外的記憶體區域。

java gc回收的是堆記憶體嗎,Java垃圾收集器(Java GC機制)與記憶體配置設定回收政策

分代收集

目前商業虛拟機的垃圾收集算法都采用“分代收集”算法。這種算法并沒有什麼新的思想,隻是根據對象存活周期不停将記憶體劃分為幾塊,這樣就可以根據各個年代的特點采用最适當的收集算法。 新生代(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的基本概念已經講完,更詳細的内容請持續關注我的部落格