我們寫代碼的時(shí)候,前端傳遞參數(shù)給后端,后端都會(huì)有一個(gè)對(duì)象來(lái)負(fù)責(zé)參數(shù)接收,同樣的JVM內(nèi)部也有一個(gè)模型來(lái)表示Java對(duì)象,而這個(gè)就是oop-Klass模型。
Hotspot虛擬機(jī)在內(nèi)部使用兩組類來(lái)表示Java的類和對(duì)象
- oop(ordinary object pointer)用來(lái)描述對(duì)象實(shí)例信息
- kclass用來(lái)描述Java類,是虛擬機(jī)內(nèi)部Java類型結(jié)構(gòu)的對(duì)等體

JVM內(nèi)部基于OOP-Klass模型描述一個(gè)Java類,將一個(gè)Java類一拆為二,第一個(gè)是oop,第二個(gè)是klass.
oop是ordinary object pointer(普通對(duì)象指針), 它用來(lái)表示對(duì)象的實(shí)例信息(Java類實(shí)例對(duì)象中各個(gè)屬性在運(yùn)行期的值)??雌饋?lái)像是一個(gè)指針,而實(shí)際上對(duì)象實(shí)例數(shù)據(jù)都藏在指針?biāo)赶虻膬?nèi)存首地址后面的一篇內(nèi)存區(qū)域中。


以上代碼在https://github.com/openjdk/jdk/blob/jdk8-b29/hotspot/src/share/vm/oops/oop.hpp
從注釋中我們可以知道oopDesc是對(duì)象類的頂級(jí)基礎(chǔ)類,{name}Desc用來(lái)描述Java對(duì)象格式,從而可以在C++中訪問到它。
Java中萬(wàn)物皆為對(duì)象,所以C++中將方法,常量,數(shù)組等都抽象成為了對(duì)象,可以用oop來(lái)表示

而klass則包含元數(shù)據(jù)和方法信息,用來(lái)描述Java類或者JVM內(nèi)部自帶的C++類型信息。比如Java類的繼承信息、成員變量、靜態(tài)變量、成員方法、構(gòu)造函數(shù)等信息都在klass中保存,JVM根據(jù)這個(gè)
可以在運(yùn)行期反射出Java類的全部結(jié)構(gòu)信息。JVM根據(jù)這個(gè)就能在運(yùn)行期反射出Java類的全部結(jié)構(gòu)信息。
同樣的和oop一樣,klass也是成體系的,它也有很多子類,這里我就不列舉出來(lái)了,我們可以看一個(gè)instanceKlass的定義

由于mirror也是一個(gè)instanceKlass,所以它包含了instanceKlass所包含的一切字段
執(zhí)行 new A() 的時(shí)候,JVM native 層里發(fā)生了什么。首先,如果這個(gè)類沒有被加載過,JVM 就會(huì)進(jìn)行類的加載,并在 JVM 內(nèi)部創(chuàng)建一個(gè) instanceKlass 對(duì)象表示這個(gè)類的運(yùn)行時(shí)元數(shù)據(jù)(相當(dāng)于 Java 層的 Class 對(duì)象)。到初始化的時(shí)候(執(zhí)行 invokespecial A::<init>),JVM 就會(huì)創(chuàng)建一個(gè)instanceOopDesc對(duì)象表示這個(gè)對(duì)象的實(shí)例,然后進(jìn)行 Mark Word 的填充,將元數(shù)據(jù)指針指向 Klass 對(duì)象,并填充實(shí)例變量。
根據(jù)對(duì) JVM 的理解,我們可以想到,元數(shù)據(jù)—— instanceKlass 對(duì)象會(huì)存在元空間(方法區(qū)),而對(duì)象實(shí)例—— instanceOopDesc 會(huì)存在 Java 堆。Java 虛擬機(jī)棧中會(huì)存有這個(gè)對(duì)象實(shí)例的引用。
handle體系
JVM內(nèi)部訪問對(duì)象并不是直接通過oop,而是通過handle,handle封裝了oop,handle是對(duì)普通對(duì)象的一種間接引用,那JVM內(nèi)部為什么要什么這種間接引用呢?
這完全是為GC考慮。
- 通過handle,能夠讓GC知道其內(nèi)部代碼有哪些地方持有GC管理對(duì)象的引用,只需要掃描handle對(duì)應(yīng)的table,這樣JVM無(wú)須關(guān)注其內(nèi)部哪些地方持有對(duì)普通對(duì)象的引用。
- GC過程中,如果發(fā)生了對(duì)象移動(dòng)(比如從新生代移動(dòng)到老年代),那么JVM內(nèi)部引用無(wú)須跟著更改為被移動(dòng)對(duì)象的新地址,JVM只需要更改handle table里面對(duì)應(yīng)的指針即可。
在JVM中為了方便回收oop和klass(oop在堆中,klass處于metaspace),會(huì)將這兩個(gè)對(duì)象封裝成 oop

https://github.com/openjdk/jdk/blob/jdk8-b29/hotspot/src/share/vm/runtime/handles.hpp
在handles.hpp被編譯后,會(huì)分別出現(xiàn)oop和klass對(duì)應(yīng)的handle,
比如
# 類實(shí)例handle
instanceHandle
# 方法實(shí)例handle
methodHandle
......
# 類元結(jié)構(gòu)handle
instanceKlassHandle
# 方法元結(jié)構(gòu)handle
methodKlassHandle
......
jvm在具體描述一個(gè)類型時(shí),會(huì)使用oop(其實(shí)這里是oopDesc)去存儲(chǔ)這個(gè)類型的實(shí)例數(shù)據(jù),使用klass去存儲(chǔ)這個(gè)類型的元數(shù)據(jù)和虛方法表。當(dāng)一個(gè)類型完成其生命周期后,需要將這兩部分都回收
,因此將 oop封裝成oop,klass也封裝成oop。
可能有點(diǎn)繞,換個(gè)說法。
比如我們將JVM內(nèi)部描述類信息模型叫做data-meta模型,將jvm內(nèi)部的oop體系的類名全部改成Data結(jié)尾,比如instanceData, methodData。
klass體系的類改成以Meta結(jié)尾,比如 methodMeta, instanceMeta, JVM再進(jìn)行GC時(shí),即要回收Data類實(shí)例,也要回收Meta類實(shí)例,為了讓GC方便回收,因此對(duì)于每一個(gè)Meta和Data類,JVM在內(nèi)部將其封裝成了oop模型。
對(duì)于Data類,內(nèi)存布局前面是oop對(duì)象頭,后面緊跟實(shí)例數(shù)據(jù);對(duì)于Meta類,其內(nèi)存布局是前面是oop對(duì)象頭,后面緊跟實(shí)例數(shù)據(jù)和方發(fā)表。 封裝成oop之后,再進(jìn)一步使用handle來(lái)封裝,于是有利于GC內(nèi)存回收。
查看Java對(duì)象在內(nèi)存中的樣子
為了讓夢(mèng)想照進(jìn)現(xiàn)實(shí),我們使用HSDB來(lái)查看JAVA對(duì)象在JVM中長(zhǎng)什么樣子,我們使用下面的代碼來(lái)進(jìn)行測(cè)試
public class MyClassTest {
private static String name = "think123";
private static int age = 18;
private static String staticString = "test";
private String desc = "heheda";
public static void main(String[] args)
throws IOException {
MyClassTest test = new MyClassTest();
hello("zhangsan");
System.in.read();
}
private static String hello(String otherName) {
return name + " : " + otherName;
}
}
我的java是安裝在 C:\Program Files\Java\jdk1.8.0_172\lib,所以在這個(gè)目錄下執(zhí)行以下命令啟動(dòng)HSDB
java -cp sa-jdi.jar sun.jvm.hotspot.HSDB
然后啟動(dòng)我們的程序(啟動(dòng)時(shí)最好關(guān)閉指針壓縮:-XX:-UseCompressedOops), 使用jps -l命令查看java進(jìn)程id, 然后選擇 File --> Attach to Hotspot Process,輸入java進(jìn)程id


查看OOP
此時(shí)我們可以查看main線程的oop以及線程堆棧,在堆棧中可以看到我們的MyClassTest對(duì)象分配到了年輕代,也可以看到對(duì)應(yīng)的內(nèi)存地址,然后把這個(gè)地址復(fù)制下來(lái),我們可以通過
Tools--> Inspector查看對(duì)應(yīng)的oop對(duì)象。

不過這種方式比較麻煩,我們可以使用Tools --> Object Histogram的方式來(lái)查看

找到對(duì)應(yīng)的對(duì)象后,雙擊打開,然后點(diǎn)擊Inspect


查看Klass


而在Method中我們則可以看到方法對(duì)應(yīng)的字節(jié)碼

常量池中這是MyClass中的常量(比如引用類全限定名,方法名,常量,LocalNumberTable,LocalVariableTable等)
通過剛才查看到的地址,我們還可以看看Klass的數(shù)據(jù)結(jié)構(gòu)是怎樣的

klass中的Method的定義: https://github.com/openjdk/jdk/blob/61bb6eca3e34b3f8382614edccd167f7ecefba65/src/hotspot/share/oops/method.hpp
可以看到,它的結(jié)構(gòu)和我們最開始列出來(lái)的instanceKlass layout是一樣的。
總結(jié)
我們需要記住的是在JVM內(nèi)部實(shí)例對(duì)象和Class是分別使用不同的數(shù)據(jù)結(jié)構(gòu)來(lái)表示的(oop,Klass),我們可以借助HSDB工具來(lái)查看它們?cè)贘VM中的數(shù)據(jù)結(jié)構(gòu)。
而我們常用到的反射,只所以能拿到那些字段,方法,接口等這些參數(shù)全是因?yàn)榻柚薑lass來(lái)實(shí)現(xiàn)的。
同時(shí)我們還可以借助它來(lái)查看常量池,比如困擾大家的intern問題,就可以結(jié)合HSDB來(lái)分析。
think123
以前最開始看小說的時(shí)候,那會(huì)兒異界修仙流比較出名,過程倒是記不住了,但是各個(gè)等級(jí)還是印象頗深。
煉氣 筑基 金丹 元嬰 離合 渡劫 大乘
如果程序員也有這種級(jí)別認(rèn)定的話,那或許也是一件不錯(cuò)的事兒,每次升級(jí)都大吼一聲
"鍵來(lái)!"
參考書籍
《揭秘Java虛擬機(jī)-JVM設(shè)計(jì)原理與實(shí)現(xiàn)》