GC基本概念和常見問題
- JVM記憶體模型及分區,每個區放什麼
- 堆裡面的分區:Eden,survival from,survival to,老年代,各自的特點
- GC的三種收集方法:标記清除、标記整理、複制算法的原理與特點,分别用在什麼地方
- Minor GC與Full GC分别在什麼時候發生
- GC是什麼
- GC收集四大算法
-
- 引用計數算法
- 複制算法 Copying
- 标記清除 Mark-Sweep
- 标記壓縮 Mark-Compact / 标記整理
- 标記清除壓縮 Mark-Sweep-Compact
- 四種引用類型
-
- 強引用 Strong Reference
- 軟引用 Soft Reference
- 弱引用 Weak Reference
- 虛引用 Phantom Reference
- 對象記憶體配置設定的一般規則
-
- 對象優先在Eden配置設定
- 大對象直接進入老年代
- 長期存活的對象将進入老年代
- 動态對象年齡判定
- 空間配置設定擔保
JVM記憶體模型及分區,每個區放什麼
堆裡面的分區:Eden,survival from,survival to,老年代,各自的特點
GC的三種收集方法:标記清除、标記整理、複制算法的原理與特點,分别用在什麼地方
Minor GC與Full GC分别在什麼時候發生
新生代GC(MinorGC):指發生在新生代的垃圾收集動作,因為Java對象大多都具備朝生夕滅的特性,是以Minor GC非常頻繁,一般回收速度也比較快。
老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC,經常會伴随至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集政策裡就有直接進行MajorGC的政策選擇過程)。Major GC的速度一般會比MinorGC慢10倍以上。
GC是什麼
分代收集算法 Generational Collection
根據對象存活周期的不同将記憶體劃分為幾塊。
新生代:大批對象死去,少量存活,使用複制算法
老年代:對象存活率高,沒有額外空間進行配置設定擔保,使用 标記清除或者标記壓縮算法
次數上頻繁收集Young區
較少收集Old區
基本不動Perm區
GC收集四大算法
引用計數算法
一般不使用
缺點:
每次對對象指派時均要維護引用計數器,且計數器本身也有一定的消耗
較難處理循環引用
複制算法 Copying
新生代中使用的是Minor GC,這種GC算法采用的是複制算法
原理:
從根集合 GC Root開始,通過Tracing從Survive From中找到存貨對象,拷貝到Survive To中
From、To交換身份,下次記憶體從To開始,重複複制的操作
優點:
沒有标記和清除的過程,效率高
沒有記憶體碎片,可以利用bump-the-pointer實作快速記憶體配置設定
缺點:需要雙倍空間
如果對象的存活率很高,假設是100%存活,那麼需要将所有的對象都複制一遍,并将所有的引用位址複制一遍。複制花費的時間,在對象多時,不可忽視。
是以複制算法要想使用,對象存活率要非常低,而且最重要的是,必須要克服50%記憶體的浪費。
實際應用中,商業JVM采用這種算法回收新生代,将記憶體分為一塊較大的Eden區和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。
當回收時,将Eden和Survivor中還存活這的對象一次性地複制到另一塊Survivor空間。
HotSpot虛拟機預設Eden和Survivor的大小比例是8:1。每次隻有10%記憶體會被“浪費”。
當Survivor空間不夠用(存活對象占用空間>10%)時,需要依賴其他記憶體(老年代)進行配置設定擔保 Handle Promotion。
标記清除 Mark-Sweep
原理:
标記 Mark,從跟集合開始掃描,對存活的對象進行标記
清楚 Sweep,掃描整個空間,回收未被标記的對象,使用free-list記錄可以區域
優點:
不需要兩倍的記憶體
缺點:
需要掃描兩次,耗時嚴重,效率低
在進行GC時,需要停止應用程式,會導緻使用者體驗差
記憶體不連續,會産生記憶體碎片,需要存入大容量對象時,不得不再次進行GC
标記壓縮 Mark-Compact / 标記整理
原理:
标記 Mark,與标記清除一樣
壓縮 Compact,再次掃描,并往一端滑動存活對象
缺點:
效率不高,不僅要标記所有粗諾對象,還要整理所有存活對象的引用位址,效率要低于複制算法
标記清除壓縮 Mark-Sweep-Compact
老年代一般使用标記清除或者标記清除與标記整理混合實作
原理:
Mark-Sweep和Mark-Compact的結合
和Mark-Sweep一緻,當進行多次GC後才Compact
記憶體效率
複制算法>标記清除算法>标記壓縮算法(此處的效率隻是對比時間複雜度,不是真是情況)
記憶體整齊度
複制算法=标記壓縮算法>标記清除算法
記憶體使用率
标記壓縮算法=标記清除算法>複制算法
年輕代 Young Gen
區域相對老年代較小,對象存活率低
這種情況複制算法的回收整理速度是最快的
複制算法的效率隻和目前存活對象的大小有關
老年代 Tenure Gen
區域較大,對象存活率高
一般用标記清除或者标記清除和标記壓縮混合實作GC
标記階段的開銷與存入對象的數量成正比
這對于老年代,标記清除或者标記壓縮有些不合适,但可以通過多核/線程利用,用并發、并行的形式提升标記效率
四種引用類型
強引用 Strong Reference
程式中普遍存在的,類似 Object obj = new Object()
隻要強引用還存在,垃圾收齊器就永遠不會回收掉被引用的對象
軟引用 Soft Reference
用來描述一些還有用但非必須的對象。在系統将要發生記憶體溢出異常之前,會把這些對象列進回收範圍中進行第二次回收。如果這次回收還沒有足夠的記憶體,才會抛出記憶體溢出異常。
弱引用 Weak Reference
也是用來描述非必須對象的,但是它的強度比軟引用更弱一些。
被弱引用關聯的對象隻能生存到下一次垃圾收集發生前。
當垃圾收集器工作時,無論目前記憶體是否足夠,都會回收掉被弱引用關聯的對象。
虛引用 Phantom Reference
也成為幽靈引用或者幻影引用,它是最若的一種引用關系。
一個對象是否有虛引用的存在,完全不會影響其生存時間,也無法通過虛引用來去的一個對象的執行個體。
為一個對象設定虛引用的位移目的就是能在這個對象被收集器回收時收到一個系統通知。
對象記憶體配置設定的一般規則
對象優先在Eden配置設定
對象在新生代Eden區中配置設定
當Eden區沒有足夠空間進行配置設定時,虛拟機将發起一次Minor GC
幾個參數設定的含義
-XX:PrintGCDetails: 收集器日志參數,告訴虛拟機在發生垃圾收集行為時列印記憶體回收日志,并且在程序退出時輸出目前的記憶體各區域配置設定情況
Xms20m: 最小堆記憶體20M
Xmx20m: 最大堆記憶體20M(堆記憶體補課擴充)
Xmn10m: 新生代10M(剩下老年代10M)
-XX:SurvivorRatio=8: 新生代中Eden區與一個Survivor區的空間比例是8:1
大對象直接進入老年代
所謂的大對象是指,需要大量連續記憶體空間的Java對象,最典型的大對象就是那種很長的字元串以及數組([byte]數組就是典型的大對象)。
大對象對虛拟機的記憶體配置設定來說就是一個壞消息(替Java虛拟機抱怨一句,比遇到一個大對象更加壞的消息就是遇到一群“朝生夕滅”的“短命大對象”,寫程式的時候應當避免),
經常出現大對象容易導緻記憶體還有不少空間時就提前觸發垃圾收集以擷取足夠的連續空間來“安置”它們。
-XX:PretenureSize Threshold參數:
令大于這個設定值的對象直接在老年代配置設定。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的記憶體複制(新生代采用複制算法收集記憶體)。
長期存活的對象将進入老年代
虛拟機給每個對象定義了一個對象年齡(Age)計數器。
如果對象在Eden出生并經過第一次Minor GC後仍然存活,并且能被Survivor容納的話,将被移動到Survivor空間中,并且對象年齡設為1。
對象在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(預設為15歲),就将會被晉升到老年代中。
對象晉升老年代的年齡閥值,可以通過參數-XX::Max Tenuring Threshold設定。
動态對象年齡判定
為了能更好地适應不同程式的記憶體狀況,虛拟機并不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代
如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuring Threshold中要求的年齡。
例如 bojA objB年齡都是2,兩個對象的記憶體大于Eden空間的一半且不能被回收,就會直接被轉移進入老年代
空間配置設定擔保
在發生Minor GC之前,虛拟機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果這個條件成立,那麼Minor GC可以確定是安全的。
如果不成立,則虛拟機會檢視HandlePromotionFailure設定值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大于曆次晉升到老年代對象的平均大小,如果大于,将嘗試着進行一次Minor GC,盡管這次Minor GC是有風險的;如果小于,或者HandlePromotionFailure設定不允許冒險,那這時也要改為進行一次Full GC。
冒險
下面解釋一下“冒險”是冒了什麼風險,前面提到過,新生代使用複制收集算法,但為了記憶體使用率,隻使用其中一個Survivor空間來作為輪換備份,是以當出現大量對象在MinorGC後仍然存活的情況(最極端的情況就是記憶體回收後新生代中所有對象都存活),就需要老年代進行配置設定擔保,把Survivor無法容納的對象直接進入老年代。與生活中的貸款擔保類似,老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的剩餘空間,一共有多少對象會活下來在實際完成記憶體回收之前是無法明确知道的,是以隻好取之前每一次回收晉升到老年代對象容量的平均大小值作為經驗值,與老年代的剩餘空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。
取平均值進行比較其實仍然是一種動态機率的手段,也就是說,如果某次Minor GC存活後的對象突增,遠遠高于平均值的話,依然會導緻擔保失敗(Handle Promotion Failure)。如果出現了HandlePromotionFailure失敗,那就隻好在失敗後重新發起一次Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是會将HandlePromotionFailure開關打開,避免Full GC過于頻繁,請讀者在JDK6Update24之前的版本中運作測試。