詳解對象的建立,佈局,定位,對象存活判斷

咱們在建立普通對象的時候只須要new關鍵字就解決了,可是在new的背後到底經歷了什麼呢?咱們建立一個對象的過程究竟是什麼樣子呢?程序員

1 對象的建立

咱們的Java虛擬機在遇到一條字節碼new指令時,首先經歷如下的步驟:算法

咱們先不介紹類加載過程,後面若是出了相關博文會在這裏給一個超連接(點擊跳轉)。緩存

在咱們的類檢查經過後,也就是到了咱們的虛擬機爲咱們的新生對象分配內存。咱們的內存分配方式有兩種安全

1.1 指針碰撞

假設Java堆中內存是絕對規整的,全部被使用過的內存都放在一邊,空閒的內存被放在另外一邊,中間放着一個指針做爲分界點的指示器,那所分配的內存就僅僅是把那個指針向空閒方向挪動一段與對象大小相等的舉例,這種分配方式稱爲「指針碰撞」(Bump ThePointer)。通常使用Serial、ParNew等帶壓縮整理過程的收集器。服務器

1.2 空閒列表

假設Java堆中的內存並非規整的,已被使用的內存和空閒的內存相互交錯在一塊兒,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄,這種分配方式稱爲「空閒列表」(Free List)。通常使用CMS這種基於清除(Sweep)算法的收集器。併發

注意,由於咱們的虛擬機建立對象是很是頻繁的,因此僅僅只是修改一個指針的位置,在併發裏也是不安全的。好比給A對象分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的狀況。咱們針對這種併發安全的問題也提出了兩種解決方案:函數

  • 同步處理。對咱們的分配內存空間的操做進行同步處理,採用CAS配上自旋的方式保證更新操做的原子性。
  • 本地線程分配緩衝。把咱們分配內存的動做按照線程劃分在不一樣的空間之中進行,也就是每一個線程在Java堆中都預先分配一小塊內存,稱爲本地線程分配緩衝區。哪一個線程要分配內存,就在哪一個線程的本地緩衝區分配,只有本地緩衝區用完了,分配新的緩衝區時才須要同步鎖定。虛擬機是否使用緩衝區,能夠經過參數-XX:+/-UseTLAB參數來設定。

分配完內存就須要將咱們的內存空間都初始化爲零值了。而後開始往咱們的對象的對象頭裏填充一些信息,好比該對象是哪一個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。至此,咱們從虛擬機的角度來看,一個對象已經產生了。可是從Java程序來看,咱們還須要進行構造函數( ()),按照按程序員的意願對對象進行初始化,一個真正可用的對象纔算徹底被構造出來。 佈局

(補充:爲了能在多數狀況下可以更快的分配內存,設計了一個叫做LinearAllocation Buffer的分配緩衝區,經過空閒列表拿到一大塊分配緩衝區以後,在它裏面仍然可使用指針碰撞方式來分配。)線程

2 對象的佈局

咱們上面介紹了到了在虛擬機中一個對象的建立,咱們接下來介紹的就是對象在堆內存中的存儲佈局。能夠劃分爲三個部分:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。設計

2.1對象頭

咱們的對象頭主要包括了兩類信息。

  • 第一類是用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等,簡稱「Mark Word」。
  • 第二類即是類型指針,即對象指向它的類型元數據的指針,Java虛擬機經過這個指針來肯定該對象是哪一個類的實例。(可是並非全部對象都會保留的,在下面定位的時候會具體談到!)

關於咱們的Mark Word會根據對象的狀態來複用空間,也就是處於什麼狀態,就會如何分配咱們的比特存儲空間。好比處於對象未被同步鎖鎖定的狀態下(無鎖態),Mark Word的32個比特存儲空間中的25個比特用於存儲對象哈希碼,4個比特用於存儲對象分代年齡,2個比特用於存儲鎖標誌位,1個比特固定爲0。下面給上其餘狀態的空間分佈:

2.2實例數據

實例數據是咱們對象真正存儲的有效信息,也就是咱們在程序代碼裏面所定義的各類類型的字段內容,不管是從父類繼承下來的,仍是在子類中定義的字段都必須記錄起來。具體的存儲順序能夠受到虛擬機分配策略參數(-XX:FieldsAllocationStyle參數)和字段在Java源碼中定義順序的影響。

2.3 對齊填充

沒有特別的含義,它僅僅起着佔位符的做用。因爲HotSpot虛擬機的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是任何對象的大小都必須是8字節的整數倍。對象頭部分已經被精心設計成正好是8字節的倍數(1倍或者2倍),所以,若是對象實例數據部分沒有對齊的話,就須要經過對齊填充來補全。

3 對象的定位

咱們建立對象是爲了後續使用對象,Java程序會經過棧上的reference數據(指向對象的引用)來操做堆上的具體對象。具體的主流對象訪問方式主要使用句柄和直接指針兩種。

3.1 句柄訪問

若是使用句柄訪問的話,Java堆中將可能會劃分出一塊內存來做爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自具體的地址信息,結構以下:

使用句柄來訪問的最大好處就是reference中存儲的是穩定句柄地址,在對象被移動(垃圾收集時移動對象是很是廣泛的行爲)時只會改變句柄中的實例數據指針,而reference自己不須要被修改。

3.2 直接指針

若是使用直接指針的話,那麼咱們的Java堆中對象的內存佈局就必須考慮如何放置訪問類型數據的相關信息,reference中存儲的直接就是對象實例數據,若是隻是訪問對象自己的話,就會了少了一次間接訪問的開銷,結構以下:

使用直接指針訪問的優勢是速度快,少了一次指針定位的時間開銷。若是對象訪問十分頻繁的話,那麼即是極爲可觀的執行成本!

4 對象存活判斷

咱們的對象在被垃圾回收器回收的時候會進行判斷對象是否存活,而後才選擇是否回收。而咱們進行判斷的兩種方式以下:

4.1 引用計數算法

在對象中添加一個引用計數器,若是被引用計數器加 1,引用失效時計數器減 1,若是計數器爲 0 則被標記爲垃圾。原理簡單,效率高,可是在 Java 中不多使用,由於存在對象間循環引用的問題,致使計數器沒法清零。

4.2 可達性分析算法

主流語言的內存管理都使用可達性分析判斷對象是否存活。基本思路是經過一系列稱爲 GC Roots 的根對象做爲起始節點集,從這些節點開始,根據引用關係向下搜索,搜索過程走過的路徑稱爲引用鏈,若是某個對象到 GC Roots 沒有任何引用鏈相連,則會被標記爲垃圾。可做爲 GC Roots 的對象包括虛擬機棧和本地方法棧中引用的對象、類靜態屬性引用的對象、常量引用的對象。

4.3 談談四種引用

在上面對象定位說到了reference是傳統的某塊內存、對象的引用。可是在JDK1.2以後,咱們的引用被細分紅了四種引用,經過強弱依次遞減分別是,強引用,軟引用,弱引用,虛引用四種。

  • 強引用。最多見的引用,例如 Object obj = new Object() 就屬於強引用。只要對象有強引用指向且 GC Roots 可達,在內存回收時即便瀕臨內存耗盡也不會被回收。
  • 軟引用。弱於強引用,描述非必需對象。在系統將發生內存溢出前,會把軟引用關聯的對象加入回收範圍以得到更多內存空間。用來緩存服務器中間計算結果及不須要實時保存的用戶行爲等。
  • 弱引用。弱於軟引用,描述非必需對象。弱引用關聯的對象只能生存到下次 YGC (Young GC)前,當垃圾收集器開始工做時不管當前內存是否足夠都會回收只被弱引用關聯的對象。因爲 YGC 具備不肯定性,所以弱引用什麼時候被回收也不肯定。
  • 虛引用。最弱的引用,定義完成後沒法經過該引用獲取對象。惟一目的就是爲了能在對象被回收時收到一個系統通知。虛引用必須與引用隊列聯合使用,垃圾回收時若是出現虛引用,就會在回收對象前把這個虛引用加入引用隊列。

5 參考資料

深刻理解Java虛擬機:JVM高級特性(第三版)

相關文章
相關標籤/搜索