jvm-Java對象建立、存儲、定位

1、對象建立

  1. 觸發 : 程序建立對象,例如clone,反序列化,new等。java

  2. 驗證類加載 : 當虛擬機接收到new指令時,檢查指令的參數可否在常量池定位到一個類的符號引用,而且檢查此符號引用的類是否已經被加載、解析、初始化過,若是沒有,則先執行對應的初始化過程。程序員

  3. 分配內存空間 : 爲新生代對象分配內存,所需內存在類加載完成後即可徹底肯定。分配內存空間即從堆中劃分一塊肯定大小的內存,此時分兩種狀況:安全

    • ①堆內存規整,使用中內存與空閒內存被一個指針隔離在兩邊,此時只須要將指針向空閒空間方向挪動此對象大小距離便可,這種方式稱爲指針碰撞;併發

    • ②若是內存不規整,使用中內存與空閒內存相互交錯,此時虛擬機須要維護一個列表,記錄哪些內存塊可用,在分配空間時須要找到一塊足夠大的空間劃分給對象實例,並更新維護的列表,這種方式爲空閒列表。oop

           選擇哪一種分配方式由java堆是否規整決定,而java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理(compact)功能決定,例如Serial、PerNew等使用指針碰撞,CMS基於Mark-Sweep採用空閒列表。
           因爲對象建立對象很是的頻繁,在併發狀況下不少操做都不是線程安全的,例如修改一個指針所指向的位置,可能爲A分配內容時還沒來得及修改指針,對象B又引用了此指針位置爲碰撞點來劃份內存,兩種解決方案:佈局

    • ①對內存分配過程同步處理-實際虛擬機採用CAS配上失敗重試的方式保證更新操做的原子性
    • ②把內存分配的動做按照線程劃分在不一樣的空間之中進行,即每一個線程在java堆中預先分配一小塊內存,稱爲本地線程分配緩衝(ThreadLocal Local Allocation Buffer,TLAB)(在1.6+纔有,在1.8默認開啓,能夠經過 jinfo -flag UseTLAB pid 來查詢),哪一個線程須要分配內存,就在那個線程的TLAB上分配,只有TLAB用完並分配新的TLAB時,才須要同步鎖定,能夠經過-XX:+/-useTLAB來開啓關閉
  4. 零值初始化 : 內存分配完成後,虛擬機須要將分配到的內存空間都初始化爲零值(不包含對象頭),這一步操做保證了對象的實例字段在java代碼中能夠不賦值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。性能

  5. 設置對象頭 : 對對應對象頭進行設置必要設置,例如這個對象屬於哪一個類,若是找到類的元數據,對象的哈希碼,對象的gc分代年齡等信息,根據虛擬機的運行狀態,若是是否啓用偏向鎖(主要爲了解決無鎖的性能問題,如今鎖基本都是可重入鎖,在A線程得到鎖後,會被標識爲偏向鎖,簡單說就是有個標記,這個線程已經獲取這個鎖了,在鎖的過程當中再競爭鎖無需進行cas等操做,不會延遲本地調用,在釋放偏向鎖的會有必定的性能損耗,但對比偏向鎖帶來的提高,整體性能仍是有提高的)等,對象頭會有不一樣的設置方式。操作系統

  6. 調用init方法 : 在上面工做完成後,從虛擬機的角度新對象已經產生了,但從java程序角度來講,對象建立纔剛剛開始,在new指令後會接着執行<init>方法,把對象按照程序員的意願進行初始化,這樣一個對象就真正的產生了。線程

2、對象內存佈局

       在HotSpot中,對象在內存中存儲的佈局可分爲3塊區域:對象頭(Header)、實例數據(Instance Data)、對齊填充(Padding)。指針

  1. 對象頭:對象頭包含兩部分信息,第一部分存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等、這部分數據的長度在32位合64位的虛擬機(未開啓壓縮指針)中分別爲32bit和64位,官方稱它爲 「Mark Word」。對象狀態在無鎖、可偏向、輕量級鎖定、重量級鎖定、GC標記、狀態下Mark Word存儲內容有所差異。
  • 偏向鎖 : A線程得到鎖,會在A線程的棧幀中變量和鎖對象的對象頭中存儲該線程ID,當該線程再次嘗試獲取該對象鎖時,不須要cas操做,只須要判斷是不是當前線程,當棧幀出棧完成,A線程並不會釋放鎖,須要等到B線程競爭該鎖才釋放,(偏向鎖的釋放須要等到全局安全點,即在此時間點沒有字節碼執行),能夠經過-XX:-UseBiasedLocking=false關閉偏向鎖,則默認會進入輕量級鎖。

  • 輕量級鎖 : A線程得到鎖,會在a線程的棧幀裏建立lock record(鎖記錄變量),讓lock record的指針指向鎖對象的對象頭中的mark word,再讓mark word 指向lock record,這就是獲取了鎖。B線程在鎖競爭時,發現鎖已經被A線程佔用,則B線程不進入內核態,讓B線程自旋,執行空循環,等待A線程釋放鎖。若是完成自旋策略仍是發現A線程沒有釋放鎖,或者讓C線程佔用了,則B線程試圖將輕量級鎖升級爲重量級鎖。

  • 重量級鎖 : 讓爭搶鎖的線程從用戶態轉換成內核態,使cpu藉助操做系統進行線程協調。

  1. 實例數據:存儲真正的有效信息,即代碼中定義的字段內容,不管是從父類中繼承下來的,仍是再子類中定義的,都須要記錄下來。HotSpot虛擬機默認的分配策略爲longs/doubles,ints,shorts/chars,bytes/booleans,oops,相同寬度的字段老是被分配到一塊兒。再知足這個前提條件下,再父類中定義的變量會出如今子類以前。

  2. 對齊填充:不是必然存在的,也沒有特別的含義,僅僅起着佔位符的做用。因爲HotSpot的自動內存管理系統要求對象起始地址必須是8字節的倍數,而對象頭部分正好時8字節的倍數(1倍或2倍),所以,當對象實例數據部分沒有對齊時,就須要經過對齊填充來補全。

3、對象訪問定位

創建對象即爲了使用對象,咱們的Java程序須要根據棧上的reference數據來操做堆上的具體對象。因爲reference類型在Java虛擬機規範中只規定了一個指向對象的引用,並無定義這個引用應該經過何種方式去定位、訪問堆中的對象的具體位置,全部具體的訪問方式取決於虛擬機實現,目前主流的訪問方式有使用句柄和直接指針兩種。

  • 句柄 : 從Java堆中劃分出一塊內存來做爲句柄池,棧幀中reference儲存句柄的地址,而句柄中包含了對象實例數據和類型數據各自的地址信息 。
  • 直接指針 : 棧幀中reference儲存對象地址,堆對象須要考慮如何訪問類型數據的相關信息。

優缺點:使用句柄時,在對象被移動(垃圾收集時移動對象很廣泛)時只會改變句柄中的實例數據指針,而reference自己不須要修改。只用直接指針速度更快,它節省了一次指針定位的時間開銷(reference->對象相對於句柄的reference->句柄->對象),因爲對象的訪問很是頻繁,所以這類開銷聚沙成塔也是一項客觀的執行成本成本。HotSpot使用直接指針來實現對象訪問。

參考文獻: 周志明.深刻理解Java虛擬機[M].第2版.北京:機械工業出版社

相關文章
相關標籤/搜索