垃圾回收(Garbage Collection,GC),顧名思義就是釋放垃圾佔用的空間,防止內存泄露。有效的使用可使用的內存,對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收。 提起java的內存回收機制,就要問三個問題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,因而它們永遠不會被引用計數器算法標記爲垃圾。bash
基於沒法解決循環引用的問題,主流的Java虛擬機裏沒有選用引用計數算法來管理內存。在Java的主流實現中,都是經過可達性算法(Reachability Analysis)來斷定對象是否存活。 在Java中:併發
tracing gc的基本思路是,以當前存活的對象集爲root,遍歷出他們(引用)關聯的全部對象(Heap中的對象),沒有遍歷到的對象即爲非存活對象,這部分對象能夠gc掉。這裏的初始存活對象集就是GC Roots。 爲何上述四種對象能夠做爲GC Roots對象可看Home3k的回答 高併發
不管是經過引用計數算法仍是可達性算法判斷對象是否存活,斷定條件都與「引用」有關。最先的Java將引用定義爲:若是reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用
。後來Java對引用的概念進行擴充,將引用分爲:學習
在肯定了「垃圾」是什麼——也就是哪些內存須要回收以後,垃圾回收器面臨的下一個問題就是——如何進行回收。因爲各個平臺的虛擬機操做內存的方法各不相同並且涉及大量的程序實現,這裏只介紹幾種算法的思想。spa
標記清除算法(Mark-Sweep)——首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象(標記過程就是上面講的對象斷定是否死亡)。執行過程以下圖: 線程
複製算法(Copying)的出現解決了標記清除算法的內存碎片問題。如今的商用虛擬機都是採用這種算法來回收新生代。它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。這樣使得每次都是對這呢個半區進行回收,即提升了回收的效率,也解決了內存碎片化的問題。複製算法的執行過程以下:
複製算法的缺點使得它只適用於對象存活率較低的新生代。 標記整理算法(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的基本概念已經講完,更詳細的內容請持續關注個人博客