JVM(四) 垃圾回收

1. 堆內存結構

 

Java堆從GC的角度能夠細分爲:新生代(Eden區、From Survivor區和To Survivor區)和老年代。java

1.1 新生代

新生代是用來存放新生的對象。通常佔據堆的1/3空間。因爲頻繁建立對象,因此新生代會頻繁觸發MinorGC進行垃圾回收。新生代又分爲Eden區、ServivorFrom、ServivorTo三個區。
  1.1.1.Eden區Java新對象的出生地(若是新建立的對象佔用內存很大,則直接分配到老年代)。當Eden區內存不夠的時候就會觸發MinorGC,對新生代區進行一次垃圾回收。
  1.1.2.ServivorFrom上一次GC的倖存者,做爲這一次GC的被掃描者。
  1.1.3.ServivorTo保留了一次MinorGC過程當中的倖存者。
  1.1.4.MinorGC的過程(複製->清空->互換)MinorGC採用複製算法。
    1:eden、servicorFrom 複製到ServicorTo,年齡+1首先,把Eden和ServivorFrom區域中存活的對象複製到ServicorTo區域(若是有對象的年齡以及達到了老年的標準,則賦值到老年代區),
    同時把這些對象的年齡+1(若是ServicorTo不夠位置了就放到老年區);
    2:清空eden、servicorFrom而後,清空Eden和ServicorFrom中的對象;
    3:ServicorTo和ServicorFrom互換最後,ServicorTo和ServicorFrom互換,原ServicorTo成爲下一次GC時的ServicorFrom區。

1.2 老年代

老年代主要存放應用程序中生命週期長的內存對象。老年代的對象比較穩定,因此MajorGC不會頻繁執行。在進行MajorGC前通常都先進行了一次MinorGC,使得有新生代的對象晉身入老年代,
致使空間不夠用時才觸發。當沒法找到足夠大的連續空間分配給新建立的較大對象時也會提早觸發一次MajorGC進行垃圾回收騰出空間。MajorGC採用標記清除算法:首先掃描一次全部老年代,標記出存活的對象,
而後回收沒有標記的對象。MajorGC的耗時比較長,由於要掃描再回收。MajorGC會產生內存碎片,爲了減小內存損耗,咱們通常須要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的時候,
就會拋出OOM(Out of Memory)異常。

2.垃圾回收機制算法

2.1 垃圾回收機制

不定時去堆內存中清理不可達對象。不可達的對象並不會立刻就會直接回收, 垃圾收集器在一個Java程序中的執行是自動的,不能強制執行,即便程序員能明確地判斷出有一塊內存已經無用了,是應該回收的,
程序員也不能強制垃圾收集器回收該內存塊。程序員惟一能作的就是經過調用System.gc 方法來"建議"執行垃圾收集器,但其是否能夠執行,何時執行卻都是不可知的。
這也是垃圾收集器的最主要的缺點。固然相對於它給程序員帶來的巨大方便性而言,這個缺點是瑕不掩瑜的。

 2.2 對象回收判斷

2.2.1 引用計數算法

引用計數法就是若是一個對象沒有被任何引用指向,則可視之爲垃圾。這種方法的缺點就是不能檢測到環的存在。 首先須要聲明,至少主流的Java虛擬機裏面都沒有選用引用計數算法來管理內存。 什麼是引用計數算法:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值加1;當引用失效時,計數器值減1.任什麼時候刻計數器值爲0的對象就是不可能再被使用的。
那爲何主流的Java虛擬機裏面都沒有選用這種算法呢?其中最主要的緣由是它很難解決對象之間相互循環引用的問題。

2.2.2  根搜索算法(可達性算法)

根搜索算法的基本思路就是經過一系列名爲」GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,
則證實此對象是不可用的。 這個算法的基本思想是經過一系列稱爲「GC Roots」的對象做爲起始點,從這些節點向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈(即GC Roots到對象不可達)時,
則證實此對象是不可用的。 那麼問題又來了,如何選取GCRoots對象呢?在Java語言中,能夠做爲GCRoots的對象包括下面幾種: (
1). 虛擬機棧(棧幀中的局部變量區,也叫作局部變量表)中引用的對象。 (2). 方法區中的類靜態屬性引用的對象。 (3). 方法區中常量引用的對象。 (4). 本地方法棧中JNI(Native方法)引用的對象。

 

 2.3 垃圾回收算法

2.3.1  標記清楚算法(Mark -Swep)

最基礎的垃圾回收算法,分爲兩個階段,標註和清除。程序員

標記階段標記出全部須要回收的對象,清除階段回收被標記的對象所佔用的空間。該算法最大的問題是內存碎片化嚴重,後續可能發生大對象不能找到可利用空間的問題。

標記清除算法的優勢和缺點

1. 優勢

- 是能夠解決循環引用的問題

- 必要時纔回收(內存不足時)

2. 缺點:

- 回收時,應用須要掛起,也就是stop the world。

- 標記和清除的效率不高,尤爲是要掃描的對象比較多的時候

- 會形成內存碎片(會致使明明有內存空間,可是因爲不連續,申請稍微大一些的對象沒法作到)

2.3.2  複製算法(copying)

爲了解決Mark-Sweep算法內存碎片化的缺陷而被提出的算法。算法

概念
若是jvm使用了coping算法,一開始就會將可用內存分爲兩塊,from域和to域, 每次只是使用from域,to域則空閒着。當from域內存不夠了,開始執行GC操做,這個時候,會把from域存活的對象拷貝到to域,
而後直接把from域進行內存清理。 應用場景 coping算法通常是使用在新生代中,由於新生代中的對象通常都是朝生夕死的,存活對象的數量並很少,這樣使用coping算法進行拷貝時效率比較高。jvm將Heap 內存劃分爲新生代與老年代,
又將新生代劃分爲Eden(伊甸園) 與2塊Survivor Space(倖存者區) ,而後在Eden –
>Survivor Space 以及From Survivor Space 與To Survivor Space 之間實行Copying 算法。
不過jvm在應用coping算法時,並非把內存按照1:1來劃分的,這樣太浪費內存空間了。通常的jvm都是8:1。也便是說,Eden區:From區:To區域的比例是始終有90%的空間是能夠用來建立對象的,
而剩下的10%用來存放回收後存活的對象。 1、當Eden區滿的時候,會觸發第一次young gc,把還活着的對象拷貝到Survivor From區;當Eden區再次觸發young gc的時候,會掃描Eden區和From區域,對兩個區域進行垃圾回收,通過此次回收後還存活的對象,
則直接複製到To區域,並將Eden和From區域清空。
2、當後續Eden又發生young gc的時候,會對Eden和To區域進行垃圾回收,存活的對象複製到From區域,並將Eden和To區域清空。 3、可見部分對象會在From和To區域中複製來複制去,如此交換15次(由JVM參數MaxTenuringThreshold決定,這個參數默認是15),最終若是仍是存活,就存入到老年代 注意: 萬一存活對象數量比較多,那麼To域的內存可能不夠存放,這個時候會藉助老年代的空間。
優缺點 優勢:在存活對象很少的狀況下,性能高,能解決內存碎片和java垃圾回收算法之
-標記清除 中致使的引用更新問題。 缺點: 會形成一部分的內存浪費。不過能夠根據實際狀況,將內存塊大小比例適當調整;若是存活對象的數量比較大,coping的性能會變得不好。

2.3.3  標記整理算法(Mark-Sweep) 也叫標記壓縮算法

 標記清除算法和標記壓縮算法很是相同,可是標記壓縮算法在標記清除算法之上解決內存碎片化問題安全

壓縮算法簡單介紹
任意順序 : 即不考慮原先對象的排列順序,也不考慮對象之間的引用關係,隨意移動對象;
線性順序 : 考慮對象的引用關係,例如a對象引用了b對象,則儘量將a和b移動到一塊;
滑動順序 : 按照對象原來在堆中的順序滑動到堆的一端。

優缺點
優勢:解決內存碎片問題,缺點壓縮階段,因爲移動了可用對象,須要去更新引用。

 2.3.4 分代算法

這種算法,根據對象的存活週期的不一樣將內存劃分紅幾塊,新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。能夠用抓重點的思路來理解這個算法。服務器

新生代對象朝生夕死,對象數量多,只要重點掃描這個區域,那麼就能夠大大提升垃圾收集的效率。另外老年代對象存儲久,無需常常掃描老年代,避免掃描致使的開銷。多線程

新生代
在新生代,每次垃圾收集器都發現有大批對象死去,只有少許存活,採用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集;

老年代
而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須「標記-清除-壓縮」算法進行回收。新建立的對象被分配在新生代,若是對象通過幾回回收後仍然存活,那麼就把這個對象劃分到老年代。
老年代區存放Young區Survivor滿後觸發minor GC後仍然存活的對象,當Eden區滿後會將存活的對象放入Survivor區域,若是Survivor區存不下這些對象,GC收集器就會將這些對象直接存放到Old區中,
若是Survivor區中的對象足夠老,也直接存放到Old區中。若是Old區滿了,將會觸發Full GC回收整個堆內存。

3.垃圾收集器

Java堆內存被劃分爲新生代和年老代兩部分,新生代主要使用複製和標記-清除垃圾回收算法;年老代主要使用標記-整理垃圾回收算法,所以java虛擬中針對新生代和年老代分別提供了多種不一樣的垃圾收集器。併發

 

 

 3.1 Serial收集器(單線程+複製算法)

Serial收集器是發展最悠久的垃圾收集器。在jdk1.3的時候只能用咱們serial垃圾回收器。他是一個單線程的垃圾回收器。用在咱們的新生代複製算法在桌面應用比較多(單線程服務器上,堆內存比較小的應用使用效率比較高).當咱們gc執行時候會暫停咱們的全部的線程這個步驟簡稱STW (Stop The World)jvm

 

 

 3.2  ParNew收集器(Serial+多線程)

 ParNew垃圾收集器實際上是Serial收集器的多線程版本,也使用複製算法,除了使用多線程進行垃圾收集以外,其他的行爲和Serial收集器徹底同樣,ParNew垃圾收集器在垃圾收集過程當中一樣也要暫停全部其餘的工做線程。性能

 

 3.3 Parallel Scavenge收集器 (簡稱PS收集器,線程程複製算法,高效)

Parallel  Scavenge收集器也是一個新生代垃圾收集器,一樣使用複製算法,也是一個多線程的垃圾收集器,它重點關注的是程序達到一個可控制的吞吐量(Thoughput,CPU用於運行用戶代碼的時間/CPU總消耗時間,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)),高吞吐量能夠最高效率地利用CPU時間,儘快地完成程序的運算任務,主要適用於在後臺運算而不須要太多交互的任務。自適應調節策略也是ParallelScavenge收集器與ParNew收集器的一個重要區別spa

-XX:MaxGCPauseMillis 垃圾回收器最大停頓時間

-XX:GCTimeRatio 吞吐量大小  (0,100) 默認最大99

3.4 Serial Old收集器(單線程標記整理算法)

 Serial Old是Serial垃圾收集器年老代版本,它一樣是個單線程的收集器,使用標記-整理算法,這個收集器也主要是運行在Client默認的java虛擬機默認的年老代垃圾收集器。

3.5.Parallel Old收集器(多線程標記整理算法)

Parallel Old收集器是Parallel Scavenge的年老代版本,使用多線程的標記-整理算法,在JDK1.6纔開始提供。在JDK1.6以前,新生代使用ParallelScavenge收集器只能搭配年老代的Serial Old收集器,只能保證新生代的吞吐量優先,沒法保證總體的吞吐量,Parallel Old正是爲了在年老代一樣提供吞吐量優先的垃圾收集器,若是系統對吞吐量要求比較高,能夠優先考慮新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略。

3.6 CMS收集器(多線程標記清楚算法)

Concurrent  mark  sweep(CMS)收集器是一種年老代垃圾收集器,其最主要目標是獲取最短垃圾回收停頓時間,和其餘年老代使用標記-整理算法不一樣,它使用多線程的標記-清除算法。

最短的垃圾收集停頓時間能夠爲交互比較高的程序提升用戶體驗。CMS工做機制相比其餘的垃圾收集器來講更復雜,整個過程分爲如下4個階段:

1.初始標記只是標記一下GC Roots能直接關聯的對象,速度很快,仍然須要暫停全部的工做線程。
2.併發標記進行GC Roots跟蹤的過程,和用戶線程一塊兒工做,不須要暫停工做線程。
3.從新標記爲了修正在併發標記期間,因用戶程序繼續運行而致使標記產生變更的那一部分對象的標記記錄,仍然須要暫停全部的工做線程。
4.併發清除清除GC Roots不可達對象,和用戶線程一塊兒工做,不須要暫停工做線程。因爲耗時最長的併發標記和併發清除過程當中,垃圾收集線程能夠和用戶如今一塊兒併發工做,
因此整體上來看CMS收集器的內存回收和用戶線程是一塊兒併發地執行。

優勢:併發收集,低停頓

缺點:佔用大量的cpu資源,沒法處理浮動垃圾,會產生碎片化

3.7 G1收集器

 Garbage  first垃圾收集器是目前垃圾收集器理論發展的最前沿成果,相比與CMS收集器,G1收集器兩個最突出的改進是:

1.基於標記-整理算法,不產生內存碎片。

2.能夠很是精確控制停頓時間,在不犧牲吞吐量前提下,實現低停頓垃圾回收。

G1收集器避免全區域垃圾收集,它把堆內存劃分爲大小固定的幾個獨立區域,而且跟蹤這些區域的垃圾收集進度,同時在後臺維護一個優先級列表,每次根據所容許的收集時間,優先回收垃圾最多的區域。區域劃分和優先級區域回收機制,確保G1收集器能夠在有限時間得到最高的垃圾收集效率。

4.內存分配策略

4.1建立的對象會優先分配到eden區域

4.2大對象直接分配到老年代

有對應的參數分配多大的對象會直接進入咱們的老年代 使用 要指定的收集器纔有效

-XX:PretenureSizeThreshold

4.3 長期存活的的對象分配到老年代

4.4 空間分配擔保

當對象生成在EDEN區失敗時,出發一次YGC,先掃描EDEN區中的存活對象,進入S0區,S0放不下的進入OLD區,再掃描S1區,若存活次數超過閥值則進入OLD區,其它進入S0區,而後S0和S1交換一次。

那麼當發生YGC時,JVM會首先檢查老年代最大的可用連續空間是否大於新生代全部對象的總和,若是大於,那麼此次YGC是安全的,若是不大於的話,JVM就須要判斷HandlePromotionFailure是否容許空間分配擔保。

容許分配擔保:

JVM繼續檢查老年代最大的可用連續空間是否大於歷次晉升到老年代的對象的平均大小,若是大於,則正常進行一次YGC,儘管有風險(由於判斷的是平均大小,有可能此次的晉升對象比平均值大不少);

若是小於,或者HandlePromotionFailure設置不容許空間分配擔保,這時要進行一次FGC。

新生代採用的是複製收集算法,S0和S1始終只是用其中一塊內存區,當出現YGC後大部分對象仍然存活的話,就須要老年代進行分配擔保,把survior區沒法容納的對象直接晉升到老年代。

那麼這種空間分配擔保的前提是老年代還有容納的空間,一共有多少對象會活下來,在實際完成內存回收以前是沒法明確知道的,因此只好取以前每次回收晉升到老年代對象容量的平均值大小做爲經驗值,與老年代的剩餘空間比較,決定是否進行FGC來讓老年代騰出更多空間。

4.5 動態對象年齡判斷

當 Survivor 空間中相同年齡全部對象的大小總和大於 Survivor 空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代,而不須要達到默認的分代年齡

相關文章
相關標籤/搜索