JVM中的物件
JVM中的物件。
一、物件的建立過程
檢查載入 -> 分配記憶體 -> 記憶體空間初始化 -> 設定 -> 物件的初始化
1. 檢查載入
虛擬機器遇到一條new指令時,首先將去檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被載入、解析和初始化過。如果沒有,那麼必須先執行相應的類載入過程。
2. 分配記憶體
根據方法區的資訊確定為該類分配的記憶體空間大小。
但是分配記憶體時主要注意兩個問題
1、如何分配空間
- 指標碰撞:假設Java堆中記憶體是絕對規整的,所有用過的記憶體都放在一邊,空閒的記憶體放在另一邊,中間放著一個指標作為分界點的指示器,那所分配記憶體就僅僅是把那個指標向空閒空間那邊挪動一段與物件大小相等的距離,這種分配方式稱為“指標碰撞”(Bump the Pointer)。
- 空閒列表:如果Java堆中的記憶體並不是規整的,已使用的記憶體和空閒的記憶體相互交錯,那就沒有辦法簡單地進行指標碰撞了,虛擬機器就必須維護一個列表,記錄上哪些記憶體塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的記錄,這種分配方式稱為“空閒列表”(Free List)。
選擇哪種分配方式由 Java 堆是否規整決定,而 Java 堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。
2、修改指標時如何實現執行緒安全
- 同步處理:對分配記憶體的空間動作進行同步處理(採用CAS配上失敗重試的方式保證跟新操作的原子性)。
- 本地執行緒分配緩衝:把記憶體分配的動作按照執行緒劃分在不同的空間之中進行,即每個執行緒在Java堆中預先分配一小塊記憶體,叫 本地執行緒分配緩衝
3. 記憶體空間初始化
注意不是構造方法
記憶體分配完成後,虛擬機器需要將分配到的記憶體空間都初始化為零值(如 int 值為 0,boolean 值為 false 等等)。這一步操作保證了物件的例項欄位在 Java 程式碼中可以不賦初始值就直接使用,程式能訪問到這些欄位的資料型別所對應的零值。
4. 設定
接下來,虛擬機器要對物件進行必要的設定,例如這個物件是哪個類的例項、如何才能找到類的元資料資訊、物件的雜湊碼、物件的 GC 分代年齡等資訊。這些資訊存放在物件的物件頭之中。
5. 物件初始化
- 在上面工作都完成之後,從虛擬機器的視角來看,一個新的物件已經產生了。
- 但從 Java 程式的視角來看,物件建立才剛剛開始,所有的欄位都還為零值。所以,一般來說,執行 new 指令之後會接著把物件按照程式設計師的意願進行初始化,這樣一個真正可用的物件才算完全產生出來。
二、物件的記憶體分佈
在 HotSpot 虛擬機器中,物件在記憶體中儲存的佈局可以分為 3 塊區域:物件頭(Header)、例項資料(Instance Data)和對齊填充(Padding)。
1、物件頭
- 物件頭包括兩部分資訊,第一部分是 Mark Word,用於儲存物件自身的執行時資料,如雜湊碼(HashCode)、GC 標誌、物件分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒 ID、偏向時間戳等。
- 物件頭的另外一部分是 型別指標,即物件指向它的類元資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。
2、例項資料
例項資料是物件真正儲存的有效資訊:程式碼中定義的各種型別的欄位內容(包含父類繼承的和子類定義的,都需要記錄下來)。
3、對齊填充
對齊填充並不是必然存在的,也沒有特別的含義,它僅僅起著佔位符的作用。
由於 HotSpot VM 的自動記憶體管理系統要求對物件的大小必須是 8 位元組的整數倍,所以當物件其他資料部分(物件例項資料)沒有對齊時,就需要通過對齊填充來補全。
例如:物件頭大小為8位元組,例項資料為5位元組,那麼例項資料就需要通過對齊填充來將例項資料補全成8位元組。
三、物件的訪問定位
建立物件是為了使用物件,我們的 Java 程式需要通過棧上的 reference 資料來操作堆上的具體物件。目前主流的訪問方式有使用控制代碼和直接指標兩種。
1、控制代碼:Java 堆中將會劃分出一塊記憶體來作為控制代碼池,reference 中儲存的就是物件的控制代碼地址,而控制代碼中包含了物件例項資料與型別資料各自的具體地址資訊。
2、直接指標:如果使用直接指標訪問, reference 中儲存的直接就是物件地址。
3、對比
- 這兩種物件訪問方式各有優勢,使用控制代碼來訪問的最大好處就是 reference 中儲存的是穩定的控制代碼地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變控制代碼中的例項資料指標,而 reference 本身不需要修改。
- 使用直接指標訪問方式的最大好處就是速度更快,它節省了一次指標定位的時間開銷,由於物件的訪問在Java中非常頻繁,因此這類開銷積少成多後也是一項非常可觀的執行成本,對 Sun HotSpot 而言,它是使用直接指標訪問方式進行物件訪問的。
四、堆記憶體的分配策略
1. 堆進一步劃分
- 新生代(PSYoungGen)
- Eden區
- From Survivor區(form區)
- To Survivor區(to區)
- 老年代(ParOldGen)
注: 設定兩個 Survivor 區是為了解決碎片化的問題(複製回收演算法)。
一些分配規則
- 新生代和老年代的記憶體分配比例大概為1:2 。
- 新生代預設分配比例是8:1:1,可以通過下面引數改變。
- -XX:SurvivorRatio=8 // 預設的8:1:1 。
- -XX:SurvivorRatio=2 // 設定比例為2:1:1 。
堆的大小可以通過如下引數設定
- -Xms20m // 設定堆初始大小20MB
- -Xmx20m // 設定堆最大的大小20MB
- -Xmn10m // 設定年輕代大小10MB
2. 分配策略
1、物件優先在Eden分配。
大多數情況下,物件在新生代 Eden 區中分配。當 Eden 區沒有足夠空間分配時,虛擬機器將發起一次 Minor GC。
2、大物件直接進入老年代。
- 虛擬機器提供引數-XX:PretenureSizeThreshold引數(這個引數只在serial和ParNew起作用),當物件比這個值大,就直接存入老年代
- 最典型的大物件是那種很長的字串以及陣列。這樣做的目的:1.避免大量記憶體複製,2.避擴音前進行垃圾回收,明明記憶體有空間進行分配。
3、長期存活的物件將進入老年代。
如果物件在 Eden 出生並經過第一次 Minor GC 後仍然存活,並且能被 Survivor 容納的話,將被移動到 Survivor 空間中,並將物件年齡設為 1,物件在 Survivor 區中每熬過一次 Minor GC,年齡就增加1,當它的年齡增加到一定程度(預設為15)時,就會被晉升到老年代中。
4、動態物件年齡判定
如果在 Survivor 空間中相同年齡所有物件大小的綜合大於 Survivor 空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代。
5、空間分配擔保
- 在發生 Minor GC 之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間,如果這個條件成立,那麼 Minor GC 可以確保是安全的。
- 如果不成立,則虛擬機器會檢視 HandlePromotionFailure 設定值是否允許擔保失敗。
- 如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小。
- 如果大於,將嘗試著進行一次 Minor GC,儘管這次 Minor GC 是有風險的,如果擔保失敗則會進行一次 Full GC 。
- 如果小於,或者 HandlePromotionFailure 設定不允許冒險,那這時也要改為進行一次 Full GC 。
五、jvm是怎麼實現泛型的
Java 語言中的泛型,它只在程式原始碼中存在,在編譯後的位元組碼檔案中,就已經替換為原來的原生型別(Raw Type,也稱為裸型別)了,並且在相應的地方插入了強制轉型程式碼,因此,對於執行期的 Java 語言來說,ArrayList<int>與 ArrayList<String>就是同一個類,所以泛型技術實際上是 Java 語言的一顆語法糖,Java 語言中的泛型實現方法稱為型別擦除,基於這種方法實現的泛型稱為偽泛型。
舉例說明:
public class Jvm3 {
public static void main(String[] args) {
Map<String,String> map = new HashMap<>();
map.put("SunnyBear","22");
System.out.println(map.get("SunnyBear"));
}
}
經過 javac 編譯成.class檔案後,使用反編譯工具檢視會發現泛型都不見了,變成了原生型別。
public class Jvm3 {
public static void main(String[] args) {
Map<String, String> map = new HashMap();
map.put("SunnyBear", "22");
System.out.println((String)map.get("SunnyBear"));
}
}
都讀到這裡了,來個 點贊、評論、關注、收藏 吧!
文章作者:IT王小二
首發地址:https://www.itwxe.com/posts/1e06d641/
版權宣告:文章內容遵循 署名-非商業性使用-禁止演繹 4.0 國際 進行許可,轉載請在文章頁面明顯位置給出作者與原文連結。