java垃圾回收
JVM內存模型
如何判斷是否垃圾
引用計數法
引用計數器的實現很簡單,對於一個對象A,只要有任何一個對象引用了A,則A的引用計數器就加1,當引用失效時,引用計數器就減1。只要對象A的引用計數器的值為0,則對象A就不可能再被使用。
引用計數法存在的問題:
- 效率問題,引用和去引用伴隨着加法與減法,影響性能
- 對於循環引用問題,無法解決
可達性分析法
從GCRoots節點一直往下走,如果走不通,說明走不通的那些對象是不可用的,那么其就可以被垃圾回收期回收。
可以作為GCRoots的對象:
- 虛擬機棧(局部變量表中對象)
- 方法區的類屬性所引用的對象
- 方法區中常量所引用的對象
- 本地方法棧中引用的的對象
垃圾回收算法
標記清除
標記-清除算法是現代垃圾回收算法的思想基礎。標記-清除算法將垃圾回收分為兩個階段:標記階段和清除階段。一種可行的實現是,在標記階段,首先通過根節點,標記所有從根節點開始的可達對象。因此,未被標記的對象就是未被引用的垃圾對象。然后,在清除階段,清除所有未被標記的對象。
缺點:
- 效率問題,標記和清除兩個過程的效率都不高
- 空間問題,標記清除后會產生大量不連續的內存碎片,空間碎片太多可能會導致以后程序在運行過程中需要分配大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。甚至會導致無法分配大對象而出現OutofMemory異常
標記整理(標記壓縮)
標記-整理算法適合用於存活對象較多的場合,如老年代。它在標記-清除算法的基礎上做了一些優化。和標記-清除算法一樣,標記-壓縮算法也首先需要從根節點開始,對所有可達對象做一次標記。但之后,它並不簡單的清理未標記的對象,而是將所有的存活對象壓縮到內存的一端。之后,清理邊界外所有的空間。
復制算法
與標記-清除算法相比,復制算法是一種相對高效的回收方法不適用於存活對象較多的場合 如新生代 將原有的內存空間分為兩塊,每次只使用其中一塊,在垃圾回收時,將正在使用的內存中的存活對象復制到未使用的內存塊中,之后,清除正在使用的內存塊中的所有對象,交換兩個內存的角色,完成垃圾回收
垃圾回收器
ParNew:可以與老年代中三個垃圾收集器都可以配合
Serial:對應Serial Old
Parallel:對應Parallel Old
Serial(串行回收)
- -XX:+UseSerailGC 開啟
- 最基本,發展最悠久的收集器
- 單線程垃圾收集器(Stop-The-World)
- 優點:簡單。對於單CPU的情況,由於沒有多線程交互開銷,反而可以更高效,是Client模式下默認的新生代收集器
- 缺點:Stop-The-World。
Parnew(並行回收)
- 復制算法(新生代收集器)
- -XX:+UseParNewGC開啟; -XX:ParallellGCThreads指定線程數
- 相比於Serial是多線程收集的,那么收集的間隔會降低一些。
- 缺點:Stop-The-World,老年代也仍然是單線程手機
Parallel Scavenge(並行回收,可控制吞吐量)
- 復制算法(新生代收集器)
- -XX:+UseParallelGC開啟
- 多線程收集
- 可以達到一個可控制的吞吐量(cpu用於運行用戶代碼的時間與cpu消耗的總時間的比值)
- -XX:MaxGCPauseMills 垃圾收集器最大停頓時間(如果設置的最大停頓時間短了,則回收的頻率就增大了咯)
- -XX:GCTimeRatio 吞吐量大小。停頓時間越短就越適合與用戶交互的應用程序,良好的響應速度能提升用戶體驗;而高吞吐量則可以高效地利用CPU時間,盡快完成程序的運算任務,主要適合在后台運算而不需要太多交互的任務。
Cms
- Concurrent Mark Sweep的縮寫,並發標記清除,大多數互聯網公司現在都在使用這個收集器
- 標記清除算法
- 工作過程:初始標記,並發標記,重新標記;並發清理
- 初始標記:該階段進行可達性分析,標記GC ROOT能直接關聯到的對象。需要STW。注意是只是標記GC ROOT直接關聯的對象那個,而對於間接關聯的對象在下一階段
- 並發標記:其是與用戶線程並發執行的過程。由初始標記階段標記過的對象出發,所有可到達的對象都在本階段標記
- 重新標記:修正並發標記期間因用戶程序繼續運行而導致標記發生改變的那一部分對象的標記。需要STW
- 並發清理:與用戶線程並發執行的過程,主要收集垃圾
- 優點:低停頓
- 缺點:占用大量CPU(當CPU數量在4個以上時,並行回收時垃圾收集線程不少於25%的CPU資源,而在不足4個,可能影響更大)。無法處理浮動垃圾(在並發清理階段用戶線程還在運行着,自然會有新的垃圾不斷產生,這一部分只能留待下一次GC再清理)。產生空間碎片(因為其使用的是標記清除算法)
- -XX:+UseConcmarkSweepGC:開啟
- -XX:ParallelCMSThreads:手工設置CMS線程個數,CMS默認啟動的線程數是(ParalleleGCThreads+3/4)
- -XX:CMSInitiatingOccupancyFraction:設置CMS收集器在老年代空間被使用多少后發出垃圾收集,默認值為68%
- -XX:+UseCMSCompactAtFullCollection:由於CMS收集器會產生碎片,此參數設置在垃圾收集后是否需要一次內存碎片整理工程
- -XX:+CMSFullGCBeforeCompaction:設置CMS收集器在進行若干次垃圾收集后再進行一次內存碎片整理過程,通過UseCMSCompactAtFullCollection一起使用
G1(Garbage First)
在G1算法中,采用了一種完全不同的方式組織堆內存,堆內存被划分為多個大小相等的內存塊(Region),每個Region是邏輯連續的一段內存。
每個Region被標記了E、S、O和H,說明每個Region在運行時都充當了一種角色,其中H是以往算法中沒有的,它代表Humongous,這表示這些Region存儲的是巨型對象(humongous object,H-obj),當新建對象大小超過Region大小一半時,直接在新的一個或多個連續Region中分配,並標記為H。
Region
堆內存中一個Region的大小可以通過-XX:G1HeapRegionSize參數指定,大小區間只能是1M、2M、4M、8M、16M和32M,總之是2的冪次方,如果G1HeapRegionSize為默認值,則在堆初始化時計算Region的實際大小。
GC模式
G1中提供了三種垃圾回收模式,yong gc,mixed gc和full gc,在不同的條件下被觸發
young gc
發送在年輕代的GC算法,一般對象(除了巨型對象)都是在eden region中分配內存,當所有eden region被耗盡無法申請內存時,就會觸發一次young gc,這種觸發機制和之前的young gc差不多,執行完一次young gc,活躍對象會被拷貝到survivor region或者晉升到old region中,空閑的region會被放入空閑列表中,等待下次被使用。
mixed gc
當越來越多的對象晉升到老年代old region時,為了避免堆內存被耗盡,虛擬機會觸發一個混合的垃圾收集器,即mixed gc,該算法並不是一個old gc,除了回收整個young region,還會回收一部分的old region,這里需要注意:是一部分老年代,而不是全部老年代,可以選擇哪些old region進行收集,從而可以對垃圾回收的耗時時間進行控制。
那么什么時候mixed gc會被觸發?
當老年代的使用率達到80%時,就會觸發一次cms gc。相對的,mixed gc中也有一個閾值參數 -XX:InitiatingHeapOccupancyPercent,當老年代大小占整個堆大小百分比達到該閾值時,會觸發一次mixed gc.
mixed gc的執行過程有點類似cms,主要分為以下幾個步驟:
- initial mark: 初始標記過程,整個過程STW,標記了從GC Root可達的對象
- concurrent marking: 並發標記過程,整個過程gc collector線程與應用線程可以並行執行,標記出GC
- remark: 最終標記過程,整個過程STW,標記出那些在並發標記過程中遺漏的,或者內部引用發生變化的對象
- 垃圾清除過程,如果發現一個Region中沒有存活對象,則把該Region加入到空閑列表中
full gc
如果對象內存分配速度過快,mixed gc來不及回收,導致老年代被填滿,就會觸發一次full gc,G1的full gc算法就是單線程執行的serial old gc,會導致異常長時間的暫停時間,需要進行不斷的調優,盡可能的避免full gc.
內存分配策略
優先分配到Eden
大多數情況下,對象在新生代Eden去中分配,但Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。
private static final int _1MB = 1024*1024;
byte[] allocation1,allocation2,allocation3,allocation4;
/**
* VM參數:-XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc - Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
*/
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB];
[GC (Allocation Failure) [DefNew: 7060K->1023K(9216K), 1.5357762 secs] 7060K->3651K(19456K), 2.2617522 secs] [Times: user=0.00 sys=0.00, real=2.26 secs]
[GC (Allocation Failure) [DefNew: 5439K->1K(9216K), 0.0492012 secs] 8067K->7748K(19456K), 0.0492559 secs] [Times: user=0.03 sys=0.00, real=0.05 secs]
Heap
def new generation total 9216K, used 4261K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 52% used [0x00000000fec00000, 0x00000000ff029020, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400570, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 7747K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 75% used [0x00000000ff600000, 0x00000000ffd90e20, 0x00000000ffd91000, 0x0000000100000000)
Metaspace used 3562K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 382K, capacity 388K, committed 512K, reserved 1048576K
結果分析:
把JVM設置了不可拓展內存20M,其中新生代10MB,老年代10MB,而新生代區域的分配比例是8:1:1,使用Serial/Serial Old組合收集器。其中,allocation1、allocation2、allocation3一共需要6M,而Eden區一共有8M,a1,a2,a3優先分配到Eden,再分配allocation4的時候Eden空間不夠,執行了一次Minor GC,由於Survivor只有1M,不夠存放a1,a2,a3,所以就直接遷移到了老年代了,后Eden空閑出來了就可以放allocation4了。
大對象直接分配到老年代
虛擬機提供了一個-XX:PretenureSizeThreshold參數來設置大對象的界限,大於此值則直接分配在老年代去了
private static final int _1MB = 1024*1024;
/**
VM參數:-XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
-XX:PretenureSizeThreshold=3145728
*/
byte[] allocation1;
allocation1 = new byte[4 * _1MB];
Heap
def new generation total 9216K, used 5012K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 61% used [0x00000000fec00000, 0x00000000ff0e5308, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
Metaspace used 3511K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 380K, capacity 388K, committed 512K, reserved 1048576K
結果分析:
the space 10240K, 40% used。大於PretenureSizeThreshold定義的闕值,所以直接分配到老年代了。
長期存活的對象會進入老年代
java虛擬機為了做到這一點,虛擬機給每個對象定義了一個對象年齡(Age)計數器。如果對象在Eden出生並經過第一次Minor GC后仍然存活,並且能被Survivor區容納的話,將被移動到Survivor空間中,並且對象年齡設為1。每熬過一次年齡就增加1.當達到(默認是15,有參數可以設置)一定程度,就會被晉升到老年代中。
private static final int _1MB = 1024*1024;
/**
* VM參數:-XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
* -XX:MaxTenuringThreshold=15 OR 1
*/
public static void testAllocation(){
byte[] allocation1,allocation2,allocation3;
allocation1 = new byte[1 * _1MB / 4];
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB];
allocation3 = null;
allocation3 = new byte[4 * _1MB];
}
MaxTenuringThreshold = 15
[GC (Allocation Failure) [DefNew: 5104K->1023K(9216K), 0.0053306 secs] 5104K->1822K(19456K), 0.0054776 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [DefNew: 5204K->1K(9216K), 0.0084990 secs] 6003K->5920K(19456K), 0.0085588 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [DefNew: 4152K->1K(9216K), 0.0006743 secs] 10071K->5920K(19456K), 0.0007392 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4297K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 52% used [0x00000000fec00000, 0x00000000ff032018, 0x00000000ff400000)
***from space 1024K, 0%*** used [0x00000000ff500000, 0x00000000ff500770, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 5918K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 57% used [0x00000000ff600000, 0x00000000ffbc79a0, 0x00000000ffbc7a00, 0x0000000100000000)
Metaspace used 3424K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 373K, capacity 388K, committed 512K, reserved 1048576K
MaxTenuringThreshold = 1
[GC (Allocation Failure) [DefNew: 5104K->1024K(9216K), 0.0057521 secs] 5104K->1848K(19456K), 0.0058778 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [DefNew: 5204K->0K(9216K), 0.0066982 secs] 6028K->5944K(19456K), 0.0067501 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [DefNew: 4151K->0K(9216K), 0.0007188 secs] 10095K->5944K(19456K), 0.0007856 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4296K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 52% used [0x00000000fec00000, 0x00000000ff0321b0, 0x00000000ff400000)
***from space 1024K, 0%*** used [0x00000000ff500000, 0x00000000ff5000c8, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 5944K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 58% used [0x00000000ff600000, 0x00000000ffbce0b8, 0x00000000ffbce200, 0x0000000100000000)
Metaspace used 3457K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 374K, capacity 388K, committed 512K, reserved 1048576K
結果分析:
從以上輸出信息可以看到,無論是MaxTenuringThreshold=15還是1,執行結果都是一樣的,至少跟書本描述的不一致。應該是JDK版本不同而有所差異
當切換到JDK6時執行相同的代碼
MaxTenuringThreshold = 15
[GC [DefNew: 4679K->375K(9216K), 0.0044310 secs] 4679K->4471K(19456K), 0.0044650 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [DefNew: 4635K->375K(9216K), 0.0086340 secs] 8731K->4471K(19456K), 0.0086660 secs] [Times: user=0.00 sys=0.02, real=0.01 secs]
Heap
def new generation total 9216K, used 4635K [0xee330000, 0xeed30000, 0xeed30000)
eden space 8192K, 52% used [0xee330000, 0xee758fe0, 0xeeb30000)
***from space 1024K, 36%*** used [0xeeb30000, 0xeeb8dc68, 0xeec30000)
to space 1024K, 0% used [0xeec30000, 0xeec30000, 0xeed30000)
tenured generation total 10240K, used 4096K [0xeed30000, 0xef730000, 0xef730000)
***the space 10240K, 40***% used [0xeed30000, 0xef130010, 0xef130200, 0xef730000)
compacting perm gen total 16384K, used 1912K [0xef730000, 0xf0730000, 0xf3730000)
the space 16384K, 11% used [0xef730000, 0xef90e3b8, 0xef90e400, 0xf0730000)
No shared spaces configured.
MaxTenuringThreshold = 1
[GC [DefNew: 4679K->375K(9216K), 0.0037650 secs] 4679K->4471K(19456K), 0.0037960 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [DefNew: 4471K->0K(9216K), 0.0010150 secs] 8567K->4471K(19456K), 0.0010580 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4423K [0xee350000, 0xeed50000, 0xeed50000)
eden space 8192K, 54% used [0xee350000, 0xee7a1fa8, 0xeeb50000)
***from space 1024K, 0%*** used [0xeeb50000, 0xeeb50000, 0xeec50000)
to space 1024K, 0% used [0xeec50000, 0xeec50000, 0xeed50000)
tenured generation total 10240K, used 4471K [0xeed50000, 0xef750000, 0xef750000)
***the space 10240K, 43***% used [0xeed50000, 0xef1adc50, 0xef1ade00, 0xef750000)
compacting perm gen total 16384K, used 1912K [0xef750000, 0xf0750000, 0xf3750000)
the space 16384K, 11% used [0xef750000, 0xef92e3b8, 0xef92e400, 0xf0750000)
No shared spaces configured.
動態對象年齡判定
為了能更好地適應不同程序的內存狀況,虛擬機並不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升到老年代,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代。
空間分配擔保
在發生Minor GC前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象的空間,如果這個條件成立,那么Minor GC可以確保安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那么會檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試着進行一次Minor GC,盡管這個Minor GC是有風險的;如果小於或者HandlePromotionFailure設置不允許冒險,那么這時也要改為進行一次Full GC了。說白了就是虛擬機避免Full GC執行的次數而去做的檢查機制。
取平均值進行比較其實仍然是一種動態概率的手段,也就是說,如果某次Minor GC存活后的對象突增,遠遠高於平均值的話,依然會導致擔保失敗(Handle Promotion Failure)。如果出現了HandlePromotionFailure失敗,那就只好在失敗后重新發起一次Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是會將HandlePromotionFailure開關打開,避免Full GC過於頻繁。
另外注意的是,在JDK 6 Update 24之后,虛擬機已經不再使用HandlePromotionFailure參數了,規則變為只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,否則將進行Full GC。
垃圾回收觸發條件
- 一個對象實例化時,先去查看Eden有沒有足夠的空間
- 如果有,不進行垃圾回收,對象直接分配在Eden存儲
- 如果Eden內存已滿,會進行一次Minor GC
- 然后再進行判斷Eden的內存是否足夠
- 如果仍然不足,則去看存活區的內存是否足夠
- 如果內存足夠,把Eden部分活躍對象保存再存活區,然后把對象保存在Eden
- 如果內存不足,向老年代發出請求,查詢老年代的內存是否足夠
- 如果老年代內存足夠,將部分存活區的活躍對象存入老年代,然后把Eden的活躍對象放入存活區,新的對象依舊保存在Eden
- 如果老年代內存不足,會進行一次full gc,之后老年代會再進行判斷 內存是否足夠,如果足夠 同上.
10.如果還不足,會拋出 OutOfMemoryError
參考博客
G1垃圾收集器介紹:https://www.jianshu.com/p/0f1f5adffdc1
《深入理解Java虛擬機》內存分配策略:https://www.cnblogs.com/wcd144140/p/5649553.html