對象的分配
虛擬機遇到一條new指令時:根據new的參數是否能在常量池中定位到一個類的符號引用,如果沒有,說明還未定義該類,拋出ClassNotFoundException;
- 檢查加載
先執行相應的類加載過程。如果沒有,則進行類加載
- 分配內存
根據方法區的信息確定爲該類分配的內存空間大小
指針碰撞 (java堆內存空間規整的情況下使用)
接下來虛擬機將爲新生對象分配內存。爲對象分配空間的任務等同於把一塊確定大小的內存從Java堆中劃分出來。
如果Java堆中內存是絕對規整的,所有用過的內存都放在一邊,空閒的內存放在另一邊,中間放着一個指針作爲分界點的指示器,那所分配內存就僅僅是把那個指針向空閒空間那邊挪動一段與對象大小相等的距離,這種分配方式稱爲“指針碰撞”。
空閒列表 (java堆空間不規整的情況下使用)
如果Java堆中的內存並不是規整的,已使用的內存和空閒的內存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄,這種分配方式稱爲“空閒列表”。
選擇哪種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。
- 內存空間初始化
(注意不是構造方法)內存分配完成後,虛擬機需要將分配到的內存空間都初始化爲零值(如int值爲0,boolean值爲false等等)。這一步操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
- 設置
接下來,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭之中。
- 對象初始化
在上面工作都完成之後,從虛擬機的視角來看,一個新的對象已經產生了,但從Java程序的視角來看,對象創建纔剛剛開始,所有的字段都還爲零值。所以,一般來說,執行new指令之後會接着把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算完全產生出來。
對象的內存佈局
- 在HotSpot虛擬機中,對象在內存中存儲的佈局可以分爲3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。
- 對象頭包括兩部分信息,第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC標誌、對象分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等。
- 對象頭的另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
- 第三部分對齊填充並不是必然存在的,也沒有特別的含義,它僅僅起着佔位符的作用。由於HotSpot VM的自動內存管理系統要求對對象的大小必須是8字節的整數倍。對象正好是9字節的整數,所以當對象其他數據部分(對象實例數據)沒有對齊時,就需要通過對齊填充來補全。
對象的訪問定位
建立對象是爲了使用對象,我們的Java程序需要通過棧上的reference數據來操作堆上的具體對象。目前主流的訪問方式有使用句柄和直接指針兩種。
- 句柄
如果使用句柄訪問的話,那麼Java堆中將會劃分出一塊內存來作爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。
- 直接指針
如果使用直接指針訪問, reference中存儲的直接就是對象地址。
這兩種對象訪問方式各有優勢,使用句柄來訪問的最大好處就是reference中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行爲)時只會改變句柄中的實例數據指針,而reference本身不需要修改。
使用直接指針訪問方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷,由於對象的訪問在Java中非常頻繁,因此這類開銷積少成多後也是一項非常可觀的執行成本。
對Sun HotSpot而言,它是使用直接指針訪問方式進行對象訪問的。
堆內存分配策略
- 新生代
Eden區
- Survivor(from)區:
設置Survivor是爲了減少送到老年代的對象
-
Survivor(to)區:
設置兩個Survivor區是爲了解決碎片化的問題(複製回收算法)
對象優先在Eden區分配
虛擬機參數:
-Xms20m 堆空間初始20m
-Xmx20m 堆空間最大20m
-Xmn10m 新生代空間10m
-XX:+PrintGCDetails 打印垃圾回收日誌,程序退出時輸出當前內存的分配情況
注意:新生代初始時就有大小
大多數情況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間分配時,虛擬機將發起一次Minor GC。
大對象直接進入老年代
-Xms20m
-Xmx20m
-Xmn10m
-XX:+PrintGCDetails
-XX:PretenureSizeThreshold=4m 超過多少大小的對象直接進入老年代
-XX:+UseSerialGC
PretenureSizeThreshold參數只對Serial和ParNew兩款收集器有效。
最典型的大對象是那種很長的字符串以及數組。這樣做的目的:1.避免大量內存複製,2.避免提前進行垃圾回收,明明內存有空間進行分配。
長期存活對象進入老年區
如果對象在Eden出生並經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並將對象年齡設爲1,對象在Survivor區中每熬過一次 Minor GC,年齡就增加1,當它的年齡增加到一定程度(默認爲15)_時,就會被晉升到老年代中。
對象年齡動態判定
如果在 Survivor空間中相同年齡所有對象大小的綜合大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代
空間分配擔保
在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那麼Minor GC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試着進行一次Minor GC,儘管這次Minor GC是有風險的,如果擔保失敗則會進行一次Full GC;如果小於,或者HandlePromotionFailure設置不允許冒險,那這時也要改爲進行一次Full GC。
HotSpot默認是開啓空間分配擔保的。
虛擬機是如何實現泛型的?
todo