03JVM-對象的產生

一、創建

Java是一門面向對象的語言,做Java程序運行過程中,無時無刻都會有對象創建出來。

當虛擬機遇到一條new指令時,經過幾個過程:

  1. 在常量池中定位到類的符號引用
  2. 檢查該符號引用對應的類是否已經被加載、解析、初始化
  3. 在Java堆中爲新生對象分配內存(對象所需內存在類加載時已經確定)

完成了以上幾個過程後,我們又開始有疑問了,對象在內存中是如何分配的呢?

  1. 先判斷是否需要在TLAB(本地線程分配緩衝)中分配
  2. 如果不需要則會在Java堆中分配

至於如何在Java堆中分配,會涉及到JVM實例啓動時所採用到的垃圾回收算法。

  1. 如果使用Serial,ParNew等帶有壓縮過程的收集器時,Java堆的劃分就爲規整,則會使用“指針碰撞”的分配方式。
  2. 如果使用CMS這種基於MAark-Sweep算法的收集器時,通常採用“空閒列表”分配方式。

明白了對象的堆中的分配情況,我們還需要了解的是對象的內存空間劃分 是需要保證線程安全的。

比如有這種情況,當對象a使用指針碰撞劃分了一塊內存空間,但是還未來得及修改指針,這時對象b也要過來分配空間,拿着舊的指針分配了一塊內存地址.
這個時候對象a和對象b的分配就會出現錯誤了。那麼虛擬機是如何解決這個問題呢?

實際上,虛擬機使用採用2中方式來解決這個問題,一是使用CAS方法+失敗不斷重試的機制;二是使用TLAB本地線程分配緩衝,在Java線程棧中分配。這兩種方式就能夠保證內存分配的併發問題。

下面解釋了“指針碰撞”和“空間列表”的內存分配方式:

指針碰撞

規整的內存空間,即所有用的內存放一邊,空閒都內存放另外一遍。中間放着一個指針作爲分界點的指示器。分配內存的過程就是把分界點指針往空閒的內存空間移動大小等於對象大小的內存空間,這種內存分配方式成爲指針碰撞

空閒列表

如果Java堆中的內存是不規整的,也就是使用過的內存和空閒內存相互交錯,那麼這種方式沒辦法用指針碰撞來分配。這時一個列表,該列表維護了內存的使用情況,包括已經使用的內存地址和空閒內存的內存地址

二、對象的數據結構

上面說完了對象的創建過程和內存分配方式,接下來說說對象在內存中的佈局。

在HotSpot虛擬機中,對象在內存中存儲的佈局結構可以分爲對象頭實例數據對其填充

1. 對象頭

對象頭包含兩部分信息

第一部分用於存儲對象自身的運行時數據

包括哈希嗎,GC分代年齡,鎖狀態,線程持有的鎖,偏向線程ID,偏向時間戳等。

第二部分類型指針

即對象指向他的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。

2. 實例數據

實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種數據類型的字段內容。無論從父類繼承還是子類自己定義的,都需要記錄起來。

實例數據的存儲順序會受到虛擬機分配參數和字段在Java源碼中定義的順序影響。

HotSpot虛擬機默認的分配策略爲longs/doubles,ints,shorts/chars,bytes,booleans,oops(ordinary object pointers), 相同寬度的字段總是被分配到了一起;除此之外,父類定義的變量會出現在子類之前。

3. 對齊填充

這部分的數據並不是必然存在的,也沒用特別的含義,它僅僅起着佔位符的作用。

由於HotSpot的自動內存管理系統要求對象起始地址必須是8字節的整倍數,換句話說,就是對象的大小必須是8字節的整數倍。

而對象頭正好是8字節的整數倍(1倍或者2倍),因此當對象實例數據部分沒有對其時,需要通過對齊填充來補全8字節的整數倍。

三、訪問定位

在Java程序中通過棧上的reference數據來操作堆上的具體對象。

目前主流的虛擬機定位和訪問堆中的對象的具體位置有兩種方式:句柄和直接指針。

1. 句柄

如果使用句柄訪問對象,虛擬機則會在Java堆中劃分一塊內存來做句柄池,reference中存儲的就是對象的句柄地址。並且句柄包含對象實例數據和類型數據各自的地址信息。

句柄訪問

這樣通過句柄就可以找到類和對象的內存位置

2. 直接指針

如果使用直接指針訪問,那麼Java堆對象的佈局中,就必須考慮如何防治訪問類型數據的相關信息,而reference中存儲的直接就是對象地址。

直接指針

兩種訪問方式各有優勢。
1. 句柄訪問方式的好處在於當對象被移動時,只需要維護句柄池中的實例數據引用地址即可,不需要改變reference的值。
2. 直接指針訪問方式好處在於節省了一次引用的定位時間。

HotSpot使用第二種方式訪問,就是直接指針訪問方式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章