一、概要
前面的文章介紹了對象的創建過程,其中第三步 —— 分配內存,只是簡單的介紹了分配的方式 —— 指針碰撞、空閑列表,其實對象在堆上分配還大有文章嘞。
對象的內存分配,往大方向上講,就是在堆上分配,對象主要分配在新生代的 Eden 區上,如果啟動了本地線程分配緩沖,將按線程優先在 TLAB 上分配。少數情況下也可能直接分配在老年代中,分配的規則並不是百分之百固定的。其細節取決於當前使用的是哪一種垃圾收集器的組合,還有虛擬機中與內存相關的參數的設置。
下面介紹一些常見的內存分配策略。
二、對象優先在 Eden/TLAB 分配
虛擬機將新生代內存分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間(默認比例是 8:1:1),大多數情況下,分配對象時,使用 Eden 和其中一塊 Survivor 空間,當沒有足夠空間進行分配時,虛擬機將會進行一次 MinorGC。
如果虛擬機打開了 TLAB,那么對象優先在 TLAB 上分配。TLAB 全稱是本地線程分配緩沖(Thread Local Allocation Buffer),它是每個線程在 Java 堆中預先分配的一小塊內存。因為 TLAB是線程私有的,沒有鎖開銷,因此性能較好,在 JDK7 之后默認開啟。
三、大對象直接進入老年代
虛擬機提供了一個 -XX:PretenureSizeThreshold 參數,令大於這個設置值的對象直接在老年代分配,這樣做的目的是避免在 Eden 區和及兩個 Survivor 區之間發生大量的內存復制。注意!該參數只對 Serial 和 ParNew 收集器有效,Parallel Scavenge 並不認識該參數。
一般我們代碼中常見的大對象是指那種很長的字符串以及數組,寫程序的時候應當避免,經常出現大對象容易導致內存還有不少空間時就提前觸發垃圾收集以獲取足夠的內存空間來“安置”它們。
四、長期存活的對象將進入老年代
還記得前面文章我們介紹對象創建的時候,說到對象頭中有一個 “GC 分代年齡” 嗎?那么這個是做啥用的呢?
如果對象在 Eden 出生並經過第一次 Minor GC 后仍然存活,並且能被 Survivor 容納的話,將被移動到 Survivor 空間中,並且對象年齡設為1。對象在 Survivor 空間中每“熬過”一次 Minor GC,年齡就增加 1 歲,當它的年齡到達一定程度(最大為 15 歲),就將會被晉升到老年代。對象晉升老年代的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold 設置。
對象是否能夠晉升到老年代,也不全由 -XX:MaxTenuringThreshold 參數控制,如果 Survivor 空間中相同年齡的所有對象大小總和大於 Survivor 空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代。
五、空間分配擔保
新生代在發生 Minor GC 之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象之和(或者歷次晉升老年代對象的平均大小)。如果這個條件不成立,那么虛擬機將直接進行 Full GC 動作;如果這個條件成立,那么虛擬機就會進行一次 Minor GC 操作,但是這次 Minor GC 是有風險的,因為比較的值是平均值,可能出現極端的情況 —— 大量對象在 Minor GC 后還存活,這時就只好在失敗后重新發起一次 Full GC。