Java對象模型

oop-klass模型

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

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

klass,用來描述 Java 類,是虛擬機內部Java類型結構的對等體 。

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

描述HotSpot中的oop 體系


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


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

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

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

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

oopDesc

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


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

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

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

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


兩模型三維度

前文講過,JVM內部基于oop-klass模型描述一個 Java 類 ,將一個 Java 類一拆為二分別描述,第一個模型是oop,第二個模型是klass。所謂oop,并不是object-oriented programming(面向對象編程),而是ordinary object pointer(普通對象指針),它用來表示對象的實例信息,看起來像個指針,而實際上對象實例數據都藏在指針所指向的內存首地址后面的一片內存區域中。 ???

而klass則包含元數據和方法信息,用來描述 Java 類而 klass 則包含元數據和方法信息,用來描述 Java 類或者JVM內部自帶的C++類型信息。其實,klass便是前文一直在講的數據結構,Java 類的繼承信息、成員變量 、靜態變量 、成員方法 、構造函數等信息都在 klass 中保存 ,JVM據此便可以在運行期反射出Java類的全部結構信息。當然,JVM本身所定義的用于描述Java類的C++類也使用klass去描述,這相當于使用另一種面向對象的機制去描述C++類這種本身便是面向對象的數據。

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

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


體系總覽

在JVM內部定義了3種結構去描述一種類型 :oop 、klass 和 handle 類。注意,這 3 種數據結構不僅能夠描述外在的 Java 類 ,也能夠描述 JVM內在的C++類型對象。

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

先上一張圖說明這種三角關系。


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

oop一般由對象頭、對象專有屬性和數據體這 3 部分構成。其一般結構如圖所示。


oop體系

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

1 ) Hotspot里的 oop 指啥

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

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

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

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

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

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

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


klass體系

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

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

?klass提供虛擬機內部的函數分發機制 。

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

與oop相同,在JVM內部也不是klass一個人在戰斗,而是一個家族。klass 家族體系如下:


handle體系

前面講過,handle封裝了oop,由于通過oop可以拿到 klass ,而 klass 是對 Java 類數據結構和方法的描述 ,因此 handle 間接封裝了 klass。JVM內部使用一個 table 來存儲 oop 指針。

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

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

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

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

1.vtable虛函數表

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

2.itable 接口函數表

itable中存放 Java 類所實現的接口類方法。同樣,JVM調用接口方法時,最終會訪問itable,找到對應的接口方法入口。

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


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

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

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

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

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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,772評論 3 422
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,947評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,960評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,350評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,549評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,104評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,914評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,089評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,340評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,834評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,106評論 2 375

推薦閱讀更多精彩內容