Java對象模型

oop-klass模型

Hotspot 虛擬機在內(nèi)部使用兩組類來表示Java的類和對象。

oop(ordinary ?object ?pointer),用來描述對象實例信息。

klass,用來描述 Java 類,是虛擬機內(nèi)部Java類型結(jié)構(gòu)的對等體 。

JVM內(nèi)部定義了各種oop-klass,在JVM看來,不僅Java類是對象,Java 方法也是對象, 字節(jié)碼常量池也是對象,一切皆是對象。JVM使用不同的oop-klass模型來表示各種不同的對象。 而在技術(shù)落地時,這些不同的模型就使用不同的 oop 類和 klass 類來表示 。由于JVM使用C/C++編寫,因此這些 oop 和 klass 類便是各種不同的C++類。對于Java類型與實例對象,只叫使用 instanceOop 和 instanceKlass 這 2 個 C++類來表示。

描述HotSpot中的oop 體系


也許是為了簡化變量名,JVM統(tǒng)一將最后的Desc去掉,全部處理成以 Oop 結(jié)尾的類型名。 例如對于 Java 類中所定義的方法 ,只明使用 methodOop 去描述 Java 方法的全部信息;對于 Java 類中所定義的引用對象變量 ,JVM則使用objArrayOop來保存這個引用變量的 “全息”信息。


縱觀以上oop和 klass 體系的定義,可以發(fā)現(xiàn),無論是 oop 還是 klass ,基本都被劃分為來分別描述 instance 、method 、constantMethod 、methodData 、array 、objArray 、typeArray 、constantPool 、 constantPoolCache 、klass 、compoiledICHolder這幾種模型,這幾種模型中的每一種都有一個對應(yīng)的 xxxOopDesc 和對應(yīng)的 xxxKlass 。通俗而言,這幾種模型分別用于描述 Java 類類型和類型指針 、Java ??方法類型和方法指針 、常量池類型及指針 、基本數(shù)據(jù)類型的數(shù)組類型及指針 、引用類型的數(shù)組類型及指針 、常量池緩存類型及指針、Java類實例對象類型及指針。Hotspot認為使用這幾種模型 ,便足以勾畫Java程序的全部 :數(shù)據(jù)、方法 、類型 、數(shù)組和實例。

那么oop到底是啥,其存在的意義究竟是什么?其名稱已經(jīng)說得很清楚,就是普通對象指 針。指針指向哪里?指向 klass 類實例。直接這么說可能比較難以理解,舉個例子,若 Java 程序中定義了一個類 ClassA ,同時程序中有如下代碼 :

Class?a =?new?ClassA ( ); ?

當(dāng)Hotspot執(zhí)行到這里時,會先將 ClassA 這個類型加載到 perm 區(qū) ( 也叫方法區(qū) ),然后在 Hotspot 堆中為其實例對象a開辟一塊內(nèi)存空間,存放實例數(shù)據(jù)。在 JVM加載ClassA到 perm 區(qū)時,JVM就會創(chuàng)建一個instanceKlass,instanceKlass中保存了 ClassA 這個 Java 類中所定義的一切信息,包括變量 、方法 、父類 、接 口、構(gòu)造函數(shù) 、屬性等,所以 instanceKlass 就是 ClassA這個Java類類型結(jié)構(gòu)的對等體。而 instanceOop ?這個“普通對象指針”對象中包含了一個指針,該指針就指向instanceKlass這個實例。在JVM實例化ClassA時,JVM又會在堆中創(chuàng)建一個instanceOop , instanceOop便是 ClassA 對象實例 a 在內(nèi)存中的對等體,主要存儲 ClassA 實例對象的成員變量。 其中,instanceOop 中有一個指針指向 instanceKlass ,通過這個指針,JVM便可以在運行期獲取這個類實例對象的類元信息。

oopDesc

既然講到了oop,就不得不提 JVM中所有oop對象的老祖宗oopDesc類。上述列表里的所有 oopDesc ,諸如 instanceOopDesc 、constantPoolOopDesc 、klassOopDesc 等 ,在 C++的繼承體系中,最終全都來自頂級的父類oopDesc ( JDK8中已經(jīng)沒有 oopDesc ,換成了別的名字,但是換湯不換藥,內(nèi)部結(jié)構(gòu)并沒有什么太大的變化)。


拋開友元類VMStructs,以及用于內(nèi)存屏障的_bs , oopDesc類中只剩下了2 個成員變量( 友元類并不算成員變量 ):mark 和 metadata。其中 metadata 是聯(lián)合結(jié)構(gòu)體,里面包含兩個元素 ,分別是 wideKlassOop 與 narrowOop,顧名思義,前者是寬指針,后者是壓縮指針。關(guān)于寬指針與窄指針這里先簡單提一句,主要用于JVM是否對Java class進行壓縮,如果使用了壓縮技術(shù), 自然可以節(jié)省出一定的寶貴內(nèi)存空間。

oopDesc的這 2 個成員變量的作用很簡單,_mark顧名思義,似乎是一種標(biāo)記,而事實上也的確如此,Java 類在整個生命周期中,會涉及到線程狀態(tài) 、并發(fā)鎖 、GC 分代信息等內(nèi)部標(biāo)識,這些標(biāo)識全都打在_mark變量上。而 _metadata顧名思義也很簡單,用于標(biāo)識元數(shù)據(jù)。每一個 Java 類都會包含一定的變量 、方法 、父類 、所實現(xiàn)的接口等信息,這些均可稱為 Java 類的“元數(shù)據(jù)”,其實可以更加通俗點,所謂的元數(shù)據(jù)就是在前面反復(fù)講的數(shù)據(jù)結(jié)構(gòu)。Java類的結(jié)構(gòu)信息在編譯期被編譯為字節(jié)碼格式,JVM則在運行期進一步解析字節(jié)碼格式,從字節(jié)碼二進制流中還原出一個Java在源碼期間所定義的全部數(shù)據(jù)結(jié)構(gòu)信息,JVM需要將解析出來結(jié)果保存到內(nèi)存中,以便在運行期進行各種操作,例如反射,而_metadata便起到指針的作用,指向 Java 類的數(shù)據(jù)結(jié)構(gòu)被解析后所保存的內(nèi)存位置。

仍然以上一節(jié)所舉的實例化ClassA這個自定義 Java 類的例子進行說明。當(dāng)JVM完成ClassA類型的實例化之后,會為該 Java 類創(chuàng)建對應(yīng)的 oop-klass 模型 ,oop 對應(yīng)的類是 instanceOop ,klass 對應(yīng)的類是 instanceKlass 。上一節(jié)講過 ,instanceOop 內(nèi)部會有一個指針指向 instanceKlass ,其實這個指針便是 oopDesc 中所定義的一_metadata。klass 是 Java類型的對等體 ,而 Java 類型 ,便是 Java 編程語言中用于描述客觀事物的數(shù)據(jù)結(jié)構(gòu),而數(shù)據(jù)結(jié)構(gòu)包含一個客觀事物的全部屬性和行為 ,所以叫做 “類元”信息,這便是_metadata的本意。

_metadata的作用可以參考下圖所示。


兩模型三維度

前文講過,JVM內(nèi)部基于oop-klass模型描述一個 Java 類 ,將一個 Java 類一拆為二分別描述,第一個模型是oop,第二個模型是klass。所謂oop,并不是object-oriented programming(面向?qū)ο缶幊蹋?,而是ordinary object pointer(普通對象指針),它用來表示對象的實例信息,看起來像個指針,而實際上對象實例數(shù)據(jù)都藏在指針?biāo)赶虻膬?nèi)存首地址后面的一片內(nèi)存區(qū)域中。 ???

而klass則包含元數(shù)據(jù)和方法信息,用來描述 Java 類而 klass 則包含元數(shù)據(jù)和方法信息,用來描述 Java 類或者JVM內(nèi)部自帶的C++類型信息。其實,klass便是前文一直在講的數(shù)據(jù)結(jié)構(gòu),Java 類的繼承信息、成員變量 、靜態(tài)變量 、成員方法 、構(gòu)造函數(shù)等信息都在 klass 中保存 ,JVM據(jù)此便可以在運行期反射出Java類的全部結(jié)構(gòu)信息。當(dāng)然,JVM本身所定義的用于描述Java類的C++類也使用klass去描述,這相當(dāng)于使用另一種面向?qū)ο蟮臋C制去描述C++類這種本身便是面向?qū)ο蟮臄?shù)據(jù)。

JVM使用 oop-klass 這種一分為二的模型描述一個 Java 類 ,雖然模型只有兩種,但是其實從 3 個不同的維度對一個 Java 類進行了描述。側(cè)重于描述 Java 類的實例數(shù)據(jù)的第一種模型 oop 主要為 Java 類生成一張 “實例數(shù)據(jù)視圖”,從數(shù)據(jù)維度描述一個Java類實例對象中各個屬性在運行期的值。而第二種模型 klass 則又分別從兩個維度去描述一個 Java 類 ,第一個維度是 Java 類的“元信息視圖”,另一個維度則是虛函數(shù)列表,或者叫作方法分發(fā)規(guī)則。元信息視圖為JVM在運行期呈現(xiàn)Java類的“全息”數(shù)據(jù)結(jié)構(gòu)信息,這是JVM在運行期得以動態(tài)反射出類信息的基礎(chǔ)。

下面的圖描述了JVM內(nèi)部對Java類的 “兩模型三維度” 的映射。


體系總覽

在JVM內(nèi)部定義了3種結(jié)構(gòu)去描述一種類型 :oop 、klass 和 handle 類。注意,這 3 種數(shù)據(jù)結(jié)構(gòu)不僅能夠描述外在的 Java 類 ,也能夠描述 JVM內(nèi)在的C++類型對象。

前面講過,klass主要描述 Java 類和 JVM內(nèi)部C++類型的元信息和虛函數(shù),這些元信息的實際值就保存在oop里面。oop 中保存一個指針指向 klass ,這樣在運行期JVM便能夠知道每一個實例的數(shù)據(jù)結(jié)構(gòu)和實際類型。handle是對 oop 的行為的封裝,在訪問 Java 類時一定是通過 handle 內(nèi)部指針得到 oop 實例的,再通過 oop 就能拿到 klass ,如此 handle 最終便能操縱 oop 的行為了(注意,如果是調(diào)用JVM內(nèi)部C++類型所對應(yīng)的oop的函數(shù) ,則不需要通過 handle 來中轉(zhuǎn),直接通過 oop 拿到指定的 klass便能實現(xiàn))。klass 不僅包含自己所固有的行為接口,而且也能夠操作 Java 類的函數(shù)。由于Java 函數(shù)在JVM內(nèi)部都被表示成虛函數(shù),因此handle模型其實就是 Java ?類行為的表達。

先上一張圖說明這種三角關(guān)系。


可以看到,Handle類內(nèi)部只有一個成員變量一handle,該變量類型是oop*,因此該變量最終指向的就是一個oop的首地址。換言之,只要能夠拿到 Handle 對象,便能據(jù)此得到其所指向的 oop 對象實例,而通過oop 對象實例又能進一步獲取其所關(guān)聯(lián)的 klass 實例,而獲取到 klass 對象實例后,便能實現(xiàn)對oop對象方法的調(diào)用。因此,雖然從表面上看,handle體系貌似是對 oop 的一種封裝 ,但是實際上其醉翁之意在于最終的 klass 體系。

oop一般由對象頭、對象專有屬性和數(shù)據(jù)體這 3 部分構(gòu)成。其一般結(jié)構(gòu)如圖所示。


oop體系

所謂oop,就是ordinary object pointer ,也即普通對象指針。但是究竟什么才是普通對象指針呢?要搞清楚何謂 oop ,要問2個問題:

1 ) Hotspot里的 oop 指啥

Hotspot里的oop 其實就是 GC 所托管的指針,每一個 oop 都是一種 xxxOopDesc*類型的指針。所有oopDesc及其子類( 除神奇的 markOopDesc 外 ) 的實例都由 GC 所管理,這才是最最重要的,是 oop 區(qū)分 Hotspot 里所使用的其他指針類型的地方。

2)對象指針之前為何要冠以“普通”二字

對象指針從本質(zhì)上而言就是一個指針,指向xxxOopDesc的指針也是普通得不能再普通的 指針,可是為何在 Hotspot 領(lǐng)域還要加一個“普通”來修飾?要回答這個問題,需要追溯到OOP( 這里的OOP 是指面向?qū)ο缶幊?)的鼻祖SmallTalk 語言。

SmallTalk語言里的對象也由 GC 來管理,但是 SmallTalk 里面的一些簡單的值類型對象都 會使用所謂的 “直接對象”的機制來實現(xiàn),例如SmallTalk里面的整數(shù)類型。所謂 “直接對象”( immediate object) 就是并不在 GC 堆上分配對象實例,而是直接將實例內(nèi)容存在對象指針里的對象。這樣的指針也叫做 “帶標(biāo)記的指針”(tagged pointer)。

這一點倒是與markOopDesc類型如出一轍,因為 markOopDesc 也是將整數(shù)值直接存儲在指針里面 ,這個指針實際上并無“指向”內(nèi)存的功能。

所以在SmallTalk的運行期 ,每當(dāng)拿到一個對象指針時,都得先校驗這個對象指針是一個直接對象還是一個真的指針?如果是真的指針,它就是一個“普通”的對象指針了。這樣對象指針就有了“普通”與“不普通”之分。

所以,在Hotspot里面 ,oop 就是指一個真的指針,而 markOop 則是一個看起來像指針但實際上是藏在指針里的對象(數(shù)據(jù))。這也正是 markOop 實例不受 GC 托管的原因,因為只要出了函數(shù)作用域,指針變量就會直接被從堆枝上釋放掉了不需要垃圾回收了。


klass體系

oop的講述先告一段落 ,再來看看 klass 部分。按照JVM的官方解釋,klass主要提供下面2種能力 :

?klass提供一個與 Java 類對等的 C++類型描述。

?klass提供虛擬機內(nèi)部的函數(shù)分發(fā)機制 。

其實這種說法與上文所說的2種維度的含義是相同的。klass 分別從類結(jié)構(gòu)和類行為這兩方面去描述一個 Java 類 ( 當(dāng)然也包含JVM內(nèi)部非開放的C++類)。

與oop相同,在JVM內(nèi)部也不是klass一個人在戰(zhàn)斗,而是一個家族。klass 家族體系如下:


handle體系

前面講過,handle封裝了oop,由于通過oop可以拿到 klass ,而 klass 是對 Java 類數(shù)據(jù)結(jié)構(gòu)和方法的描述 ,因此 handle 間接封裝了 klass。JVM內(nèi)部使用一個 table 來存儲 oop 指針。

如果說oop是對普通對象的直接引用,那么 handle 就是對普通對象的一種間接引用,中間隔了一層。但是JVM內(nèi)部為何要使用這種間接引用呢?答案是,這完全是為GC考慮。具體表現(xiàn)在2個地方 :

通過handle,能夠讓 GC 知道其內(nèi)部代碼都有哪些地方持有 GC 所管理的對象的引用,這只需要掃描 handle 所對應(yīng)的 table ,這樣 JVM 便無須關(guān)注其內(nèi)部到底哪些地方持有對普通對象的引用。

在GC過程中如果發(fā)生了對象移動(例如從新生代移到了老年代),那么JVM的內(nèi)部引用無須跟著更改為被移動對象的新地址,JVM 只需要更改 handle table 里對應(yīng)的指針即可 。

當(dāng)然實際的handle作為對 Java 類方法的訪問的包裝,遠不止上面所描述的這么簡單。這里涉及 Java 類的類繼承和接口繼承的話題,在 C++領(lǐng)域,類的繼承和多態(tài)性最終通過vptr(虛函數(shù)表)來實現(xiàn)。在klass內(nèi)部,記錄了每一個類的vptr信息,具體而言分為兩部分來描述。

1.vtable虛函數(shù)表

vtable中存放 Java 類中非靜態(tài)和非 private 的方法入口,JVM調(diào)用 Java 類的方法 (非靜態(tài)和非 private)時,最終會訪問vtable,找到對應(yīng)的方法入口。

2.itable 接口函數(shù)表

itable中存放 Java 類所實現(xiàn)的接口類方法。同樣,JVM調(diào)用接口方法時,最終會訪問itable,找到對應(yīng)的接口方法入口。

不過要注意,vtable和itable 里面存放的并不是Java類方法和接口方法的直接入口,而是指向了 Method 對象入口,JVM會通過Method最終拿到真正的 Java 類方法入口,得到方法所對應(yīng)的字節(jié)碼/二進制機器碼并執(zhí)行。當(dāng)然,對于被JIT進行動態(tài)編譯后的方法,JVM最終拿到的是其對應(yīng)的被編譯后的本地方法的入口。


這里有個問題,前面不是一直在說handle是對 oop 的直接封裝和對 klass 的間接封裝嗎,為什么這里卻分別給 oop 和 klass 定義了 2 套不同的 handle 體系呢?這給人的感覺好像是,封 裝 oop 的 handle 和封裝 klass 的 handle 并不是同一個 handle ,既然不是同一個handle ,那么通 過封裝 oop 的handle 還怎么去得到所對應(yīng)的 klass 信息呢?

其實這正是只怕內(nèi)部常常容易使人迷惑的地方。在JVM中,使用oop-klass這種一分為二的模型去描述 Java 類以及 只叫內(nèi)部的特殊類群體,為此JVM內(nèi)部特定義了各種oop和 klass類型。但是,對于每一個oop,其實都是一個 C++類型,也即 klass;而對于每一個 klass 所對應(yīng)的 class ,在JVM內(nèi)部又都會被封裝成 oop。只怕在具體描述一個類型時,會使用 oop 去存儲這個類型的實例數(shù)據(jù),并使用 klass 去存儲這個類型的元數(shù)據(jù)和虛方法表。而當(dāng)一個類型完成其生命周期后,JVM會觸發(fā) GC 去回收,在回收時,既要回收一個類實例所對應(yīng)的實例數(shù)據(jù) oop , 也要回收其所對應(yīng)的元數(shù)據(jù)和虛方法表(當(dāng)然,兩者并不是同時回收,一個是堆區(qū)的垃圾回收, 一個是永久區(qū)的垃圾回收)。為了讓 GC 既能回收 oop 也能回收 klass,因此 oop 本身被封裝成了 oop ,而 klass 也被封裝成 oop。而只叫內(nèi)部恰好將描述類實例的 oop 全都定義成類名以 oop 結(jié)尾的類,并將描述類結(jié)構(gòu)和方法信息的 klass 全都定義成類名以 klass 結(jié)尾的類 ,而只怕內(nèi)部描述類信息的模型恰巧也叫作 oop-klass,與類名存在重合,這就導(dǎo)致了很多人的疑惑,這些疑惑完全是因為叫法上的重合而產(chǎn)生。

因此為了進一步解開疑惑,我們不妨換個叫法,不再將JVM內(nèi)部描述類信息的模型叫作

oop-klass,而是叫作 data-meta 模型 (瞎取的名字沒啥特殊含義)。然后將JVM內(nèi)部的 oop 體系的類名全都改成以 Data結(jié)尾 ,例如,methodData 、instanceData 、constantPoolData 等,同時 將 klass 體系的類名也全都改成以 Meta 結(jié)尾,例如methodMeta 、instanceMeta 、constantPoolMeta 等。JVM在進行 GC 時,既要回收 Data 類實例,也要回收 Meta 類實例,為了讓 GC 便于回收,因此對于每一個 Data 類和每一個 Meta 類 ,JVM在內(nèi)部都將其封裝成了 oop 模型。對于 Data 類,其內(nèi)存布局是前面為 oop 對象頭 ,后面緊跟實例數(shù)據(jù);而對 Meta 類 ,其內(nèi)存布局是前面為 oop 對象頭,后面緊跟實例數(shù)據(jù)和虛方法表。封裝成 oop 之后,再進一步使用 handle 來封裝, 于是便有利于 GC 內(nèi)存回收。

在這種新的模型中,不管是Data類還是 Meta 類,都是一種普通的 C++類型,只不過它們從不同的角度對 Java 類進行了描述。不管是 Data 類還是 Meta 類,當(dāng)其所在的JVM的內(nèi)存區(qū)域爆滿后,都會觸 GC,為了方便回收,因此就需要將其封裝成 oop。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容