最近在看 Java 虛擬機方面的資料,以備工做中的不時之需。首先我先拋出一個我本身想的面試題,而後再引出後面要介紹的知識點如逃逸分析、標量替換、棧上分配等知識點java
Java 對象必定分配在堆上嗎?
本身先思考下,再往下閱讀效果更佳哦!面試
咱們都知道 Java 對象通常分配在堆上,而堆空間又是全部線程共享的。瞭解 NIO 庫的朋友應該知道還有一種是堆外內存也叫直接內存。直接內存是直接向操做系統申請的內存區域,訪問直接內存的速度通常會優於堆內存。直接內存的大小不直接受 Xmx 設定的值限制,可是在使用的時候也要注意,畢竟系統內存有限,堆內存和直接內存的總和依然仍是會受操做系統的內存限制的。segmentfault
經過上面的分析,你們也知道了,Java 對象除了能夠分配在堆上,還能夠直接分配在堆外內存中。但這點不是我今天想討論的,我想和你們聊聊棧上分配,說到棧上分配就不得不先說下逃逸分析數組
逃逸分析是是一種動態肯定指針動態範圍的靜態分析,它能夠分析在程序的哪些地方能夠訪問到指針。jvm
換句話說,逃逸分析的目的是判斷對象的做用域是否有可能逃出方法體性能
判斷依據有兩個學習
- 對象是否被存入堆中(靜態字段或堆中對象的實例字段)
- 對象是否被傳入未知代碼中(方法的調用者和參數)
咱們來分析下這兩個依據優化
對於第一點對象是否被存入堆中,咱們知道堆內存是線程共享的,一旦對象被分配在堆中,那全部線程均可以訪問到該對象,這樣即時編譯器就追蹤不到全部使用到該對象的地方了,這樣的對象就屬於逃逸對象,以下所示spa
public class Escape { private static User u; public static void alloc() { u = new User(1, "baiya"); } }
User 對象屬於類 Escape 的成員變量,該對象是可能被全部線程訪問的,因此會發生逃逸操作系統
第二點是對象是否被傳入未知代碼中,Java 的即時編譯器是以方法爲單位進行編譯,即時編譯器會把方法中未被內聯的方法當成未知代碼,因此沒法判斷這個未知方法的方法調用會不會將調用者或參數放到堆中,因此認爲方法的調用者和參數是逃逸的,以下所示
public class Escape { private static User u; public static void alloc(User user) { u = user; } }
方法 alloc 的參數 user 被賦值給類 Escape 的成員變量 u,因此也會被全部線程訪問,也是會發生逃逸的。
棧上分配是 Java 虛擬機提供的一種優化技術,該技術的基本思想是能夠將線程私有的對象打散,分配到棧上,而非堆上。那分配到棧上有什麼好處呢?
咱們知道棧中的變量會在方法調用結束後自動銷燬,因此省掉了 jvm 進行垃圾回收,進而能夠提升系統的性能
棧上分配是要基於逃逸分析和標量替換實現的
咱們經過一個具體的例子來驗證下非逃逸分析的對象確實是分配到了棧上
public class OnStack { public static void alloc() { User user = new User(1, "baiya"); } public static void main(String[] args) { long start = Instant.now().toEpochMilli(); for (int i = 0; i < 100_000_000; i++) { alloc(); } long end = Instant.now().toEpochMilli(); System.out.println("耗時:" + (end - start)); } }
上面的代碼是循環 1 億次執行 alloc 方法建立 User 對象,每一個 User 對象佔用約 16 bytes(怎麼計算的下面會說) 空間,建立 1 億次,因此若是 User 都是在堆上分配的話則須要 1.5G 的內存空間。若是咱們設置堆空間小於這個數,應該會發生 gc,若是設置的特別小,應該會發生大量的 gc。
咱們用下面的參數執行上述代碼
-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+EliminateAllocations其中 -server 是開啓 server 模式,逃逸分析須要 server 模式的支持
-Xmx10 -Xms10m,設置堆內存是 10m,遠小於 1.5G
-XX:+DoEscapeAnalysis 開啓逃逸分析
-XX:+PrintGCDetails 若是發生 gc,打印 gc 日誌
-XX:+EliminateAllocations 開啓標量替換,容許把對象打散分配在棧上,好比 User 對象,它有兩個屬性 id 和 name,能夠把他們當作獨立的局部變量分別進行分配
配置好 jvm 參數後,執行代碼,查看結果可知執行了 3 次 gc,耗時 10 毫秒,能夠推斷出 User 對象並未所有分配到堆上,而是把絕大多數分配到了棧上,分配在棧上的好處是方法結束後自動釋放對應的內存,是一種優化手段。
咱們上面說了棧上分配依賴逃逸分析和標量替換,那麼咱們能夠破壞其中任意一個條件,去掉逃逸分析就能夠經過 -XX:-DoEscapteAnalysis 或者關閉標量替換 -XX:-EliminateAllocations 再去執行上述代碼,觀察執行狀況,發現發生了大量的 gc,而且耗時 3182 毫秒,執行時間遠遠高於上面的 10 毫秒,因此能夠推測出並未執行棧上分配的優化手段
對象由四部分構成
- 對象頭:記錄一個對象的實例名字、ID和實例狀態。
普通對象佔用 8 bytes,數組佔用 12 bytes (8 bytes 的普通對象頭 + 4 bytes 的數組長度)
- 基本類型
boolean,byte 佔用 1 byte
char,short 佔用 2 bytes
int,float 佔用 4 bytes
long,double 佔用 8 bytes
- 引用類型:每一個引用類型佔用 4 bytes
- 填充物:以 8 的倍數計算,不足 8 的倍數會自動補齊
咱們上面的 User 對象有兩個屬性,一個 int 類型的 id 佔用 4 bytes,一個引用類型的 name 佔用 4bytes,在加上 8 bytes 的對象頭,正好是 16 bytes
關於虛擬機的知識點還有不少並且也比較重要,若是懂對寫優質代碼、優化性能、排查問題等都是錦上添花,好比逃逸分析,即時編譯器會根據逃逸分析的結果進行優化,如所消除以及標量替換。感興趣的朋友能夠本身查查資料學習下。經過這個棧上分配的例子,之後咱們寫代碼時,把能夠不逃逸的對象寫進方法體中,這樣就會被編譯器優化,提高性能。並且也知道了上面面試題的答案,就是 Java 中的對象並必定分配在堆上,也可能分配在棧上
- 《實戰Java虛擬機》
- 《深刻理解Java虛擬機》
- https://zh.wikipedia.org/wiki...
歡迎關注公衆號 【天天曬白牙】,獲取最新文章,咱們一塊兒交流,共同進步!