你說你熟悉JVM?那你知道Java對象是如何創建、存儲和訪問的嗎?

雲棲號資訊:【點擊查看更多行業資訊
在這裏您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!


前言

Java程序員都知道如何創建對象,不就是一個Person person = new Person()的語句就解決了麼?然而,我們只知道new,卻對於底層如何實現對象的創建、如何存儲到內存中去、又如何被訪問的知之甚少。

對象的創建

流程圖

1

創建流程

Java程序new一個對象。

虛擬機遇到一條new指令時,首先檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,且檢查該符號引用代表的類是否已被加載、解析和初始化過。若沒有,需先進行相應的類加載過程。

在類加載檢查通過後,虛擬機將爲新生對象分配內存。(對象在內存中所需要的大小在類加載完成後就確定了)

內存分配完之後,虛擬機需要將分配到的內存空間初始化爲零值(不包括對象頭)。保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,可以訪問對應的零值。(對應準備階段)

虛擬機對對象進行必要的設置(對象頭的設置)。如這個對象是哪個類的實例、如何找到類的元數據信息、對象哈希碼、對象的GC分代年齡等信息。

以上虛擬機中新對象產生,對應到Java程序還需要繼續執行方法,將對象在程序中進行初始化。

內存空間分配方式

爲對象分配空間就是從Java堆中劃分出一塊確定大小的內存給新生對象,考慮符合劃分可用空間的兩種方式:“指針碰撞”和“空閒列表”

  • 指針碰撞:若Java堆中內存是絕對規整的,所有用過的內存都放在一邊,空閒的內存放在另一邊,中間放着一個指針作爲分界點的指示器,所分配內存僅僅是把那個指針向空閒空間那邊挪動一段與對象大小相等的距離。在使用Serial、ParNew收集器時等帶有Compact過程時,系統分配算法是指針碰撞。
  • 空閒列表:Java堆中內存不是規整的,已使用的內存和空閒的內存相互交錯,VM需維護一個列表,記錄上哪些內存是可用的,在分配時從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄。使用CMS收集器時,就是採用的空閒裏列表,CMS是基於Mark-Sweep算法(標記-清除)的收集器。

併發安全問題

Java對象創建在程序中是非常常見的,所以在VM中對象創建是非常頻繁,容易出現多線程併發安全問題:如程序中創建對象A和對象B,底層VM給A對象分配內存,指針沒來及修改,對象B同時使用原來的指針分配內存。

解決方案有兩種:同步處理和本地線程分配緩衝

  • 同步處理:分配內存空間的動作進行同步處理(CAS操作),VM採用CAS配上失敗重試的方式保證更新操作的原子性;
  • 本地線程分配緩衝:Thread Local Allocation Buffer, TLAB,把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存,即爲TLAB,哪個線程要分配內存,就在哪個線程的TLAB上分配,只有用完後並分配新的TLAB,才需要同步鎖定。通過-XX:+/-UseTLAB參數設定是否需要使用TLAB。

對象的內存佈局

概述

Java對象在內存存儲的佈局分爲3塊:對象頭、實例數據和對齊填充。

對象頭

對象頭(Header)分爲兩部分:用於存儲對象自身的運行時數據和類型指針。

運行時數據

Mark Word,用於存儲對象自身的運行時數據包括:哈希碼、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等。

2

Mark Word是一個非固定的數據結構,在極小的空間內存儲儘量多的數據,會根據對象的狀態複用自己的存儲空間,如在32位HotSpot VM中,若對象處於未鎖定狀態,Mark Word的32bit空間中25bit用於存儲對象哈希碼,4bit用於存儲對象分代年齡,2bit用於存儲鎖標誌位,1bit固定爲0,即32(存儲空間)=25(哈希碼)+4(分代年齡)+2(鎖標誌位)+1(固定0)

類型指針

即對象指向它的類元數據的指針,虛擬機通過這個指針來確定對象是哪個類的實例,但是並非查找對象的元數據就一定要通過對象本身,也只是適用於普通對象,普通Java對象可以通過元數據信息可以確定Java對象的大小。不適用的Java對象,如Java數組對象的對象頭中必須有一塊能保持記錄數組長度的數據,因爲從數組元數據中無法確定數組的大小。

實例數據

實例數據(Instance Data)是對象真正存儲的有效信息,也是程序代碼中定義的各種類型的字段內容。這部分存儲順序會受到VM分配策略參數和字段在Java源碼中定義順序的影響。

VM默認分配策略

HotSpot默認分配策略爲longs/doubles、ints、shorts/chars、bytes/nooleans、oops,相同寬度的字段會被分配到一起,在父類中定義的變量會出現在子類之前。

對齊填充

對齊填充(Padding)是非必要的,只是起着佔位符的作用。VM自動內存管理系統要求對象起始地址(對象大小)必須是8字節的整數倍,對象頭都是8字節的整數倍,而實例數據部分若沒有8字節的整數倍,可以通過對齊填充進行補全。

對象的訪問方式

概述

Java程序通過棧上的reference數據類操作堆上的具體對象(棧中的局部變量表存儲了對象名的變量,堆中存儲了對象的具體地址)。主流的對象訪問定位方式有兩種:使用句柄和直接指針。

使用句柄

使用句柄訪問對象,Java堆中會劃分出一塊內存作爲句柄池,reference中存儲的就是對象的句柄地址,句柄中包含了對象實例數據與類型數據各自的具體地址信息。

3

直接指針

使用直接指針訪問,Java堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,而reference中存儲的直接就是對象地址。(Sun HotSport VM的使用方式)

4

訪問方式對比

使用句柄訪問優勢是reference中存儲的是穩定的句柄地址,對象被移動時,只會改變句柄中實例數據指針,reference本身不會變;

使用直接指針訪問優勢是速度快,節省一次指針定位時間開銷。(JVM默認使用)

【雲棲號在線課堂】每天都有產品技術專家分享!
課程地址:https://yqh.aliyun.com/live

立即加入社羣,與專家面對面,及時瞭解課程最新動態!
【雲棲號在線課堂 社羣】https://c.tb.cn/F3.Z8gvnK

原文發佈時間:2020-07-20
本文作者:程序員偉傑
本文來自:“掘金”,瞭解相關信息可以關注“掘金”

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