HSDB: 16張圖帶你看見JVM中的Java對(duì)象

我們寫代碼的時(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ì)象

  1. oop(ordinary object pointer)用來(lái)描述對(duì)象實(shí)例信息
  2. kclass用來(lái)描述Java類,是虛擬機(jī)內(nèi)部Java類型結(jié)構(gòu)的對(duì)等體
class-in-jvm

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ū)域中。


other-oops.png
oopDesc.png

以上代碼在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)表示

other-oops

而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的定義

layout

由于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考慮。

  1. 通過handle,能夠讓GC知道其內(nèi)部代碼有哪些地方持有GC管理對(duì)象的引用,只需要掃描handle對(duì)應(yīng)的table,這樣JVM無(wú)須關(guān)注其內(nèi)部哪些地方持有對(duì)普通對(duì)象的引用。
  2. 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

handle.png

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

attach

查看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ì)象

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

查看Klass

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


常量池中這是MyClass中的常量(比如引用類全限定名,方法名,常量,LocalNumberTable,LocalVariableTable等)

通過剛才查看到的地址,我們還可以看看Klass的數(shù)據(jù)結(jié)構(gòu)是怎樣的

hsdb-klass-structure.png

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)》

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

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

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