本系列筆記主要基於《深入理解Java虛擬機:JVM高級特性與最佳實踐 第2版》,是這本書的讀書筆記。 記憶體分配一般關註的是對象在堆上分配的情況,對象主要分配在新生代的Eden區中,如果啟用了本地線程分配緩衝,將按線程優先在TLAB上分配。少數情況下也會直接分配在老年代中,這取決於使用的垃圾收集器組合 ...
本系列筆記主要基於《深入理解Java虛擬機:JVM高級特性與最佳實踐 第2版》,是這本書的讀書筆記。
記憶體分配一般關註的是對象在堆上分配的情況,對象主要分配在新生代的Eden區中,如果啟用了本地線程分配緩衝,將按線程優先在TLAB上分配。少數情況下也會直接分配在老年代中,這取決於使用的垃圾收集器組合,以及設置的JVM參數。
對象優先在Eden分配
大多數情況下,對象在Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機會發起一次Minor GC。
- Minor GC:發生在新生代的垃圾收集,因為一般大多對象具有朝生夕滅的特性,所以Minor GC發生非常頻繁,回收速度也比較快。
- Major GC/Full GC:發生在老年代的垃圾收集,發生了Major GC,Major GC的回收速度比較慢。
當Minor GC時,發現Eden中已存在的對象,也無法放入Survivor區,那麼就會通過分配擔保機制提前轉移到老年代。
大對象直接進入老年代
大對象一般是指需要大量連續記憶體空間的Java對象,最典型的大對象就是那種很長的字元串或數組,例如new byte[10 * 1024 * 1024]
是一個10M的大對象。遇到大對象對虛擬機來說是可怕的,更可怕的是遇到一群朝生夕滅的短命大對象。經常遇到大對象,會導致在記憶體還有不少空間時就提前觸發垃圾收集,用以獲取足夠的連續記憶體空間來安置大對象。
虛擬機提供了一個參數-XX:PretenureSizeThreshold
,大於這個參數的對象會直接在老年代分配。這樣能避免在Eden區和兩個Survivor區之間大量的記憶體複製(新生代採用複製演算法)。
長期存活的對象進入老年代
虛擬機採用了分代收集的思想來回收記憶體,既然這樣,那記憶體回收時,虛擬機需要能識別哪些對象應該放到新生代,哪些對象應該放到老年代。為了做到這點,虛擬機給每個對象定義了一個對象年齡計數器。
如果對象在Eden區出生後經過一次Minor GC仍然存活,並且能被Survivor區容納的話,那麼對象將被移動到Survivor空間,同時對象年齡設為1。對象在Survivor區中每熬過一次Minor GC,對象年齡就增加1歲。當對象年齡達到一定值後(預設15),對象就會晉升到老年代中。這個晉升到老年代的年齡閾值,可以通過參數-XX:MaxTenuringThreshold
設置。
動態對象年齡判定
有時為了適應不同的記憶體情況,虛擬機並不是死板的按照-XX:MaxTenuringThreshold
的值,要求對象必須達到年齡閾值才能晉升老年代。
如果在Survivor空間中,所有相同年齡的對象的大小總和,大於Survivor空間的一半,那麼年齡大於或等於該年齡的對象,都可以直接進入老年代,無須等到-XX:MaxTenuringThreshold
設置的年齡閾值。
空間分配擔保
新生代採用了複製收集演算法,為了提高記憶體利用率,通常將新生代劃分為了1個Eden區,和2個相同大小的Survivor區,它們的大小比例是8:1:1。這樣其中的一個Survivor區來作為輪換備份,當出現大量對象在Minor GC後仍然存活時(極端情況例如所有對象都存活),Survivor區無法容納,這時就需要老年代進行分配擔保,將Survivor無法容納的對象直接進入老年代。
老年代進行這樣的分配擔保,前提是老年代本身還有容納這些對象的剩餘空間。而有多少對象能存活下來在記憶體回收之前是無法準確預知的,所以只好根據之前每次回收晉升到老年代對象容量的平均大小,作為經驗值,與老年代剩餘空間進行比較。如果經驗值大於剩餘空間,這時老年代空間不足,就會發起一次Full GC。如果經驗值小於剩餘空間,就會Minor GC,這時新生代記憶體回收實際發生後,如果晉升到老年代時老年代空間不足,就會擔保失敗,擔保失敗則會再發起Full GC。
可見,擔保失敗後再Full GC,繞的圈子是最大的。按照現在分配擔保的規則,只要老年代的連續空間大於新生代對象總大小,或歷次晉升的平均大小,就會進行Minor GC,否則進行Full GC。