HotSpot虚拟机在Java堆中对对象的管理
在大概了解了Java虛擬機中內(nèi)存的大致分布后,接下來就應該了解虛擬機是如何在內(nèi)存中管理對象的,畢竟Java是一門面向對象的語言,在Java程序的運行過程中會不斷有對象創(chuàng)建出來。為了方便,這里僅僅以HotSpot虛擬機和Java堆內(nèi)存為例,介紹下HotSpot虛擬機在Java堆中對象分配、布局和訪問的過程。
1、對象的創(chuàng)建
在Java語言中,我們可以使用new關鍵字創(chuàng)建一個對象(這里僅僅討論普通的Java對象,不包括數(shù)組和Class對象等),在虛擬機中,創(chuàng)建對象的過程比我們想象的要復雜一些。
在虛擬機創(chuàng)建對象之前會加載類,這部分也比較重要,這里先略過這部分,假設要創(chuàng)建的對象已經(jīng)加載成功。我們從虛擬機的角度來考慮,如果要創(chuàng)建一個對象,要考慮這幾個問題:
- 創(chuàng)建對象就要內(nèi)存,在哪里分配內(nèi)存?
- 有了可以分配內(nèi)存的地方,然后內(nèi)存如何分配呢?
- 實際中對象創(chuàng)建非常頻繁,如何保證分配內(nèi)存時的線程安全?
- 分配完內(nèi)存后如何設置內(nèi)存要存儲的內(nèi)容?
首先,Java堆是線程共享的,大多數(shù)對象都會在這里創(chuàng)建,所以虛擬機也會在這里創(chuàng)建對象。但是Java堆是一塊內(nèi)存區(qū)域,在從這塊內(nèi)存中分出一塊可用的空間來創(chuàng)建對象時有兩種分配方式:指針碰撞和空閑列表。這兩種分配方式基于堆內(nèi)存是否是規(guī)整的來選擇的。如果Java堆中的內(nèi)存絕對規(guī)整,所有用過的內(nèi)存在一邊,所有沒用過的內(nèi)存在一邊,那么就可以維護一個作為中間分界線的指針,需要分配內(nèi)存時,只需要將分界線向空閑方向移動相應的距離即可。
如果Java堆中的內(nèi)存不是規(guī)整的,已經(jīng)使用的內(nèi)存和沒有使用的內(nèi)存相互交錯,那么就需要虛擬機維護一個未被使用的空間的列表,即空閑列表,這里記錄哪些內(nèi)存是可用的,分配的時候找出一塊夠用的空間進行佩芬,并在分配后更新這個列表。
那么如何選擇這兩種方式呢?看起來是由Java堆是否規(guī)整決定,但Java堆是否規(guī)整又是由所采用的垃圾收集器是否帶有壓縮整理功能決定的。這就涉及到了Java垃圾回收機制,這里不過多介紹。
雖然Java中沒有指針,但是虛擬機在內(nèi)存中還是會使用指針的。就是說,分配一塊內(nèi)存后,用一個指針表示這塊內(nèi)存。這樣,如果當多個線程同時創(chuàng)建對象時,可能會出現(xiàn)這樣的問題:正在給對象A分配內(nèi)存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內(nèi)存。
對于這個問題,有兩個解決方法。實際上虛擬機采用CAS加上失敗重試的方式保證更新操作的原子性;另一種是使用本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)。TLAB是每個線程在Java堆中預先分配的一小塊內(nèi)存,哪個線程要分配內(nèi)存,就在那個線程的TLAB上分配,只有TLAB上不夠并分配新的TLAB時才需要同步。
內(nèi)存分配后,虛擬機將分配的內(nèi)存空間都初始化為零值(不包括對象頭,對象頭在后面介紹)。如果使用TLAB,那這個工作就在TLAB分配時進行。這樣就保證了對象的實例字段在Java代碼中可以不賦初值就直接使用,程序能訪問到這些字段的數(shù)據(jù)類型對應的零值。
之后,虛擬機要對對象進行必要的設置,比如這個對象是哪個類的實例、如何才能找到類的元數(shù)據(jù)、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象頭中。
這是,在虛擬機看來,一個新的對象就創(chuàng)建完成了,不過,對象還沒有初始化,所有的實例字段還都是零值,即對于Java程序來說,對象的創(chuàng)建才剛開始。然后執(zhí)行對象的構造函數(shù),將對象按照類的構造函數(shù)所期望的進行初始化,這樣,一個對象就創(chuàng)建完了。
2、對象的內(nèi)存布局
上面介紹了對象是如何創(chuàng)建的,那么分配的那塊內(nèi)存到底存了什么呢?
在HotSpot虛擬機中,對象在內(nèi)存中存儲的布局可以分為3塊:對象頭(Header)、實例數(shù)據(jù)(Instance Data)和對齊數(shù)據(jù)(Padding)。
對象頭包含兩部分,第一部分用于存儲對象自身的運行時數(shù)據(jù),和哈希碼、GC分代年齡、鎖狀態(tài)標志、線程持有的鎖、偏向鎖ID、偏向時間戳等,這部分數(shù)據(jù)的長度在32位和64為位的虛擬機中分別是32位和64位,官方叫“Mark Word”。Mark Word結構如下:
由于對象要存儲的運行時數(shù)據(jù)很多,已經(jīng)超過了限制的長度,Mark Word被設計成了非固定的數(shù)據(jù)結構,以便在極小的空間內(nèi)存儲更多的數(shù)據(jù),它會根據(jù)對象的狀態(tài)復用自己的存儲空間,如上圖。
對象頭的另一部分是類型指針,即對象指向它的類元數(shù)據(jù)的指針,虛擬機可以通過這個指針來確定這個對象屬于哪個類。并不是所有的虛擬機實現(xiàn)都必須在對象數(shù)據(jù)上保留類型指針,也就是說查找對象的元數(shù)據(jù)信息不一定要經(jīng)過對象本身。另外,如果對象是一個Java數(shù)組,那么對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù),因為虛擬機可以根據(jù)普通Java對象的元數(shù)據(jù)信息確定對象的大小,但是從數(shù)組的元數(shù)據(jù)中無法確定數(shù)組的大小。
接下來的數(shù)據(jù)是對象真正存儲的有效信息,也是程序代碼中所定義的各種類型的字段的內(nèi)容。無論是從父類繼承來的,還是子類中定義的,都要記錄下來。這部分的存儲順序會受到虛擬機分配策略參數(shù)和字段在類中定義的順序的影響。HotSpot虛擬機默認的分配策略是longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),可以看出,相同寬度的字段總是分配到一起。在這個情況下,父類的字段會在子類之前。
第三部分是填充數(shù)據(jù),這部分不是必須的,僅僅起到占位符的作用。HotSpot虛擬機的自動內(nèi)存管理系統(tǒng)要求對象的起始地址必須是8字節(jié)的整數(shù)倍,如果不夠的話,就需要填充來補齊。
3、對象的訪問定位
上面的兩部分解決了如何分配內(nèi)存以及在內(nèi)存中存放什么數(shù)據(jù)的問題。經(jīng)過這兩個步驟就創(chuàng)建好了一個Java對象,但創(chuàng)建對象是為了使用的。在Java虛擬機棧中有局部變量表,用來存儲一個方法要用到的局部數(shù)據(jù)。對于基本類型的數(shù)據(jù)可以直接存放數(shù)據(jù),但是對于對象實例就不可以了,因為對象種類太多也不能確定大小。這時可以用reference引用類型表示一個對象實例,這個reference指向Java堆中創(chuàng)建好的對象,就可以使用了。
不過,這個Java堆中的數(shù)據(jù)要使用時還需要知道所屬的類的元數(shù)據(jù)信息,比如這個對象是屬于哪個類的。這時,如何確定對象的類型數(shù)據(jù)就有兩個方法:使用句柄訪問和使用直接指針訪問。
如果使用句柄訪問,那么Java堆中就會劃出一塊內(nèi)存來作為句柄池,reference中存放的就是對象的句柄地址,而句柄中包括了對象實例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息,如下圖:
這樣,reference就可以找到實例數(shù)據(jù)和類型數(shù)據(jù)了。
如果使用直接指針訪問的話,那么Java堆對象的布局就需要存放類型數(shù)據(jù)的相關信息了,而reference中存放的就是對象地址,如下圖:
這兩種對象訪問方式各有優(yōu)勢,使用句柄來訪問的最大好處就是reference中存儲的就是穩(wěn)定的句柄地址,在對象被移動(垃圾回收中這種情況經(jīng)常發(fā)生)時只會改變句柄中的實例數(shù)據(jù)指針,而reference本身不需要修改。
使用直接指針訪問的方式的最大好處就是速度更快,因為它節(jié)省了一次指針定位的時間開銷,由于Java中對象的訪問非常頻繁,這樣的積累還是有很客觀的性能提升的。而HotSpot中就是使用的這種訪問方式。
添加公眾號Machairodus,我會不時分享一些平時學到的東西~
總結
以上是生活随笔為你收集整理的HotSpot虚拟机在Java堆中对对象的管理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java内存区域分布
- 下一篇: 当贝max1不认4t硬盘