引言
我們知道,一般在java程序中,new的對象是分配在堆空間中的,但是實際的情況是,大部分的new對象會進入堆空間中,而並非是全部的對象,還有另外兩個地方可以存儲new的對象,我們稱之為棧上分配以及TLAB
棧上分配
為什么需要棧上分配?
在我們的應用程序中,其實有很多的對象的作用域都不會逃逸出方法外,也就是說該對象的生命周期會隨着方法的調用開始而開始,方法的調用結束而結束,對於這種對象,是不是該考慮將對象不在分配在堆空間中呢?
因為一旦分配在堆空間中,當方法調用結束,沒有了引用指向該對象,該對象就需要被gc回收,而如果存在大量的這種情況,對gc來說無疑是一種負擔。
什么是棧上分配?
因此,JVM提供了一種叫做棧上分配的概念,針對那些作用域不會逃逸出方法的對象,在分配內存時不在將對象分配在堆內存中,而是將對象屬性打散后分配在棧(線程私有的,屬於棧內存)上,這樣,隨着方法的調用結束,棧空間的回收就會隨着將棧上分配的打散后的對象回收掉,不再給gc增加額外的無用負擔,從而提升應用程序整體的性能
本質:Java虛擬機提供的一項優化技術
基本思想: 將線程私有的對象打散分配在棧上
優點:
1)可以在函數調用結束后自行銷毀對象,不需要垃圾回收器的介入,有效避免垃圾回收帶來的負面影響
2)棧上分配速度快,提高系統性能
局限性: 棧空間小,對於大對象無法實現棧上分配
技術基礎: 逃逸分析
逃逸分析的目的: 判斷對象的作用域是否超出函數體[即:判斷是否逃逸出函數體
//user的作用域超出了函數setUser的范圍,是逃逸對象 //當函數結束調用時,不會自行銷毀user private User user; public void setUser(){ user = new User(); user.setId(1); user.setName("blueStarWei"); } //u只在函數內部生效,不是逃逸對象 //當函數調用結束,會自行銷毀對象u public void createUser(){ User u = new User(); u.setId(2); u.setName("JVM"); }
public class AllotOnStack { public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i < 100000000; i++) { alloc(); } long end = System.currentTimeMillis(); System.out.println(end - start); } private static void alloc() { User user = new User(); user.setId(1); user.setName("blueStarWei"); } }
上述代碼調用了1億次alloc(),如果是分配到堆上,大概需要1.5GB的堆空間,如果堆空間小於該值,必然會觸發GC。
使用如下參數運行,發現不會觸發GC
// 使用server模式 最大堆空間為15m 初始堆空間為15m 啟用逃逸分析 打印GC日志 關閉TLAB 啟用標量替換,允許對象打散分配到棧上
-server -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:+EliminateAllocations
使用如下參數(任意一行)運行,會發現觸大量GC
//關閉逃逸分析 關閉TLAB 啟用標量替換,允許對象打散分配到棧上 -server -Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:+EliminateAllocations //啟用逃逸分析 關閉TLAB 關閉標量替換 -server -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:-EliminateAllocations
可以發現:棧上分配依賴於逃逸分析和標量替換
GC日志
[GC (Allocation Failure) 4095K->528K(15872K), 0.0025208 secs] [GC (Allocation Failure) 4624K->552K(15872K), 0.0012518 secs] [GC (Allocation Failure) 4648K->608K(15872K), 0.0009262 secs] ......(省略) 3718
GC日志解析

JVM參數解析

JVM在內存新生代Eden Space中開辟了一小塊線程私有的區域,稱作TLAB(Thread-local allocation buffer)。默認設定為占用Eden Space的1%。在Java程序中很多對象都是小對象且用過即丟,它們不存在線程共享也適合被快速GC,所以對於小對象通常JVM會優先分配在TLAB上,並且TLAB上的分配由於是線程私有所以沒有鎖開銷。因此在實踐中分配多個小對象的效率通常比分配一個大對象的效率要高。
也就是說,Java中每個線程都會有自己的緩沖區稱作TLAB(Thread-local allocation buffer),每個TLAB都只有一個線程可以操作,TLAB結合bump-the-pointer技術可以實現快速的對象分配,而不需要任何的鎖進行同步,也就是說,在對象分配的時候不用鎖住整個堆,而只需要在自己的緩沖區分配即可。
關於對象分配的JDK源碼可以參見 JVM 之 Java對象創建[初始化] 中對OpenJDK源碼的分析。
一個100KB的TLAB區域,如果已經使用了80KB,當需要分配一個30KB的對象時,TLAB是如何分配的呢?
此時,虛擬機有兩種選擇:第一,廢棄當前的TLAB(會浪費20KB的空3.4 間);第二,將這個30KB的對象直接分配到堆上,保留當前TLAB(當有小於20KB的對象請求TLAB分配時可以直接使用該TLAB區域)。
JVM選擇的策略是:在虛擬機內部維護一個叫refill_waste的值,當請求對象大於refill_waste時,會選擇在堆中分配,反之,則會廢棄當前TLAB,新建TLAB來分配新對象。
【默認情況下,TLAB和refill_waste都是會在運行時不斷調整的,使系統的運行狀態達到最優。】

棧上分配和TLAB對比
Java對象分配的過程
- 編譯器通過逃逸分析,確定對象是在棧上分配還是在堆上分配。如果是在堆上分配,則進入選項2.
- 如果tlab_top + size <= tlab_end,則在在TLAB上直接分配對象並增加tlab_top 的值,如果現有的TLAB不足以存放當前對象則3.
- 重新申請一個TLAB,並再次嘗試存放當前對象。如果放不下,則4.
- 在Eden區加鎖(這個區是多線程共享的),如果eden_top + size <= eden_end則將對象存放在Eden區,增加eden_top 的值,如果Eden區不足以存放,則5.
- 執行一次Young GC(minor collection)。
- 經過Young GC之后,如果Eden區任然不足以存放當前對象,則直接分配到老年代。
對象不在堆上分配主要的原因還是堆是共享的,在堆上分配有鎖的開銷。無論是TLAB還是棧都是線程私有的,私有即避免了競爭(當然也可能產生額外的問題例如可見性問題),這是典型的用空間換效率的做法。
對象內存分配的兩種方法
為對象分配空間的任務等同於把一塊確定大小的內存從Java堆中划分出來。
指針碰撞(Serial、ParNew等帶Compact過程的收集器)
假設Java堆中內存是絕對規整的,所有用過的內存都放在一邊,空閑的內存放在另一邊,中間放着一個指針作為分界點的指示器,那所分配內存就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離,這種分配方式稱為“指針碰撞”(Bump the Pointer)。
空閑列表(CMS這種基於Mark-Sweep算法的收集器)
如果Java堆中的內存並不是規整的,已使用的內存和空閑的內存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間划分給對象實例,並更新列表上的記錄,這種分配方式稱為“空閑列表”(Free List)。
總結
總體流程
對象分配流程
如果開啟棧上分配,JVM會先進行棧上分配,如果沒有開啟棧上分配或則不符合條件的則會進行TLAB分配,如果TLAB分配不成功,再嘗試在eden區分配,如果對象滿足了直接進入老年代的條件,那就直接分配在老年代。
對象在內存的引用方式
對象在內存中的結構
引用文章: