Java的技術體系包括
- 支持Java程序運行的虛擬機(JVM)
- 提供接口支持的Java API
- Java 編程語言
- 第三方Java框架(如Spring等)
Java與C++之間有一堵由內存動態分配和垃圾收集技術所圍成的高墻,墻外面的人想進去,墻里面的人想出來。
Java虛擬機的自動內存管理,其實解決的是兩個問題:給對象分配內存、回收不再使用的內存。內存的回收算法在前面的文章中已經闡述,我們來看一下虛擬機是如何給對象分配內存的。給對象分配內存,其實就是在堆上進行分配,通常情況下對象主要會分配在新生代的Eden區,如果啟動了本地線程分配緩沖,將按線程優先分配在TLAB上。少數情況下也可能直接將對象分配在老年代中。
內存分配的規則是由垃圾收集器組合以及內存相關的參數設置兩方面因素共同決定的。同時,內存分配也會共同遵守一些通用的策略,這些策略如下
對象優先分配在Eden區
大多數情況下,對象在新生代的Eden區中分配,當新生代沒有足夠空間時,虛擬機將發起一次MinorGC。
- MinorGC,新生代GC,指發生在新生代的垃圾回收行為。因為java中絕大多數對象都具備朝生夕死的特性,所以MonorGC非常頻繁,一般回收速度也較快。
- MajorGC(或者成為FullGC),老年代GC,指發生在老年代的垃圾回收行為,出現了MajorGC經常會伴隨至少一次的MinorGC。MajorGC的速度一般會比MinorGC的速度慢十倍以上。
大對象直接進入老年代
大對象是指需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組。大對象對于虛擬機而言是一個不友好的存在,因為經常出現大對象會導致內存還有不少空間時為了獲取足夠空間放置大對象而不得不提前觸發垃圾回收行為。
虛擬機提供了一個參數 -XX:PretenureSizeThreshold,意思是當對象所需的內存空間大于這個值的時候,直接將對象分配在老年代。這樣做可以避免在Eden區和兩個Survivor區之間發生大量的內存復制,從而避免效率的降低。
長期存活的對象將進入老年代
虛擬機采用了分代收集的思想來管理內存,因此虛擬機給每個對象定義了對象年齡計數器,這部分內容存儲在對象頭(Header)中。對象年齡的定義是這樣的:如果對象在Eden區出生,并且經過一次MinorGC之后仍然存活,之后被移動到Survivor區,此時對象年齡設置為1,當對象在Survivor區中每經歷過一次MinorGC并且依然存活,則年齡加1。
當對象的年齡增加到一定程度,默認為15歲,將會被放到到老年代中。這個年齡閾值,可以通過參數 -XX:MaxTenuringThreshold 設置。
動態對象年齡判定
為了更好地適應不同程序的內存狀況,并不是只有對象年齡達到設置值才會進入老年代。如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,那么年齡大于或等于該年齡的對象就可以直接進入老年代。
空間分配擔保
在發生MinorGC之前,虛擬機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間。如果這個條件成立,那么MinorGC可以確保是安全的。如果不成立,虛擬機會查看 HandlePromotionFailur 的設置值是否允許擔保失敗,如果允許,那么會繼續檢查老年代的最大連續可用內存空間是否大于歷次進入老年代對象容量的平均大小,如果大于,將嘗試一次MinorGC,如果此時擔保失敗,會在失敗后發起一次FullGC。如果小于或者設置值不允許進行冒險,這時也會進行一次FullGC。
HandlePromotionFailur設置值一般都會設置為允許擔保失敗,以避免頻繁的FullGC。
總結
內存分配原則
- 對象優先分配在Eden區
- 大對象直接進入老年代
- 長期存活的對象,或者相同年齡對象之和大于Survivor空間一半的對象以及更老的對象,進入老年代
內存回收的時機(When 什么時候開始回收)
- 新生代沒有足夠空間時,進行MinorGC
- 擔保失敗,或者不允許擔保失敗,或者老年代剩余最大連續可用空間小于歷次進入老年代對象大小的平均值,則進行FullGC
內存回收什么對象(What 回收些什么)
通過可達性分析算法找出的,到GCRoots之間沒有任何引用鏈的對象
內存回收如何發起(怎么找到GCRoots開始回收)
以HosSpot虛擬機為例,通過OopMap-安全點-安全區域的一套機制來快速準確的完成對GC Roots的枚舉
內存如何回收(How 怎么進行回收)
通過分代收集算法思想,將虛擬機內存分為
- 新生代,新生代采用復制算法進行回收,因此新生代又被分為
- Eden區,80%空間
- Survivor區,10%空間
- Survivor區,10%空間
- 老年代,采用標記復制的算法進行回收。