類和對象在JVM中是如何存儲的,竟然有一半人回答不上來!

前言

這篇部落格主要來說說類與對象在JVM中是如何存儲的,由於JVM是個非常龐大的課題,所以我會把他分成很多章節來細細闡述,具體的數量還沒有決定,當然這不重要,重點在於是否可以在文章中學到東西,是否對JVM可以有一些更深的理解,當然這也是筆者自己寫文章的初衷。

問題提出

我們在日常工作學習中所使用的Java語言,其最大的特點就是「跨平台」,我們不用在不同的平台上編譯兩套不同的機器碼,而可以做到「一次編譯,到處運行」,其跨平台最重要的一個因素就在於,Java語言並不直接運行在真實機器上,而是有一個虛擬機(即Java Virtual Machine ,JVM)來承載其運行,我們通過javac命令,將.java文件編譯成為.class文件,然後通過虛擬機來編譯/解釋執行成對應的平台硬編碼並執行,使得只要安裝了該虛擬機的平台,就可以運行java程式。

實際上,現在不光Java可以運行在Java虛擬機上,還有例如Kotlin、Scala、Groovy、Clojure等語言,都採用了這種模式,編譯成為class文件後,放在Java虛擬機上運行,所以筆者預計在很長的一段時間內,即使Java會過時,但是Java虛擬機也會存在較長的一段時間。

那麼就從最開始說起,我們寫程式時,最先進行的操作一定是新建一個類,然後新建一個對象,那麼類與對象在JVM中是如何存儲的呢

如何窺探?

在研究這個問題之前,我們必須要看到類和對象在JVM中是以何種狀態存在的,在筆者經過一段時間的學習後,了解了JDK自帶的一款「神器」—HSDB,下面來介紹其基本的一些使用方式。

啟動

首先需要需要複製jdkjrebin目錄下的sawindbg.dll文件到jrebin目錄下,然後進入jdklib目錄下,使用java -cp .sa-jdi.jar sun.jvm.hotspot.HSDB,即可啟動HSDB:

類和對象在JVM中是如何存儲的,竟然有一半人回答不上來!啟動HSDB

然後我們啟動一個Java項目,讓其保持啟動狀態:

  public class Blog {
      public static void main(String[] args) {
          System.out.println("Hello JVM");
  
          while(true){}
      }
  }

 

在終端中使用jps -l命令,查看運行起來的Java進程的進程號。

類和對象在JVM中是如何存儲的,竟然有一半人回答不上來!jps查看進程

我這裡的進程號是720,獲取到進程號之後,點擊HSDB上的File->Attach to HotSpot Process,並輸入進程號:

類和對象在JVM中是如何存儲的,竟然有一半人回答不上來!HSDBAttach

點擊【OK】,即可綁定進程,下圖中是這個Java進程中的所有執行緒。

類和對象在JVM中是如何存儲的,竟然有一半人回答不上來!綁定進程成功

查看類

我們可以通過這個工具,來看一下我們剛才運行的這個類究竟是以何種形式,存在於JVM中的。

點擊Tools -> Class Browser,然後可以找到Main方法所在類的記憶體地址,可以看到我創建的類的記憶體地址是0x7c0060828

類和對象在JVM中是如何存儲的,竟然有一半人回答不上來!查看類

然後點擊Tools -> Inspector,在右上方輸入記憶體地址,就可以看到這個類的數據了。

類和對象在JVM中是如何存儲的,竟然有一半人回答不上來!查看類數據

到這裡我們已經可以看到,我們所創建的類,其在記憶體中的存在形式,實際上是使用一個名為InstanceKlass的類的實例進行存儲的。我們可以得到一個並不是太準確的結論,也算是到目前為止的一個認知,類在JVM中,是被InstanceKlass所描述的,InstanceKlass中包含類的元數據和方法資訊,例如:Java類的繼承資訊成員變數靜態變數成員方法構造函數等,JVM可以通過InstanceKlass來反射出Java類的全部結構資訊。

查看對象

在HSDB中,我們找到類的記憶體地址後,通過Inspector可以清楚地看到類在JVM中的一種存在形式。實際上在我們第一次學Java的時候,就聽過一句話:在Java中,萬物皆對象,在JVM看來,不僅Java對象是對象,Java類也是對象,Java方法也是對象,位元組碼常量池皆為對象。

由於JVM是由C++編寫,所以我們在Java中聲明的所有東西,都可以在由C++編寫的JVM中以一個對象的方式存在,正如一個Java類是以InstanceKlass的一個實例對象來表示一樣,Java對象也可以使用一個C++對象來表示,我們可以來重複一次上述的過程,來看看Java對象是如何在JVM中進行存儲的。

首先我們需要修改剛才的測試程式碼:

  public class Blog {
      public static void main(String[] args) {
          //在Main方法中新建一個對象
          Blog blog = new Blog();
  
          while(true){}
      }
  }

我們在Main方法中新建了一個Blog對象,然後在HSDB中查看這個對象在JVM中是怎樣的:類和對象在JVM中是如何存儲的,竟然有一半人回答不上來!

找到創建的對象:

類和對象在JVM中是如何存儲的,竟然有一半人回答不上來!Main執行緒堆棧內容類和對象在JVM中是如何存儲的,竟然有一半人回答不上來!找到執行緒堆棧中對象

可以看到在JVM中,對象是以一個名為Oop的對象來描述的,在Oop對象中,有一個_metadata,代表這個對象的類元數據,其中有一個compressed_klass指針,指向的正是我們上文中說的,描述類的元資訊的InstanceKlass

相信在上面一些小小的測試中,我們應該都有了一些基本的認知。無論是Java中的類,還是對象,在JVM中都是以對象的形式存在的,存放類的InstanceKlass對象,保存了類的元數據,例如父類、方法、成員變數、靜態變數等等,而Oop對象中保存了對象的一些資訊,了解過對象的記憶體分布的同學應該知道一個Java對象中存放有哪些結構,但是這裡先賣個關子,這部分內容會在後期文章中單獨敘述,還有一個指向類元數據InstanceKlass的指針。現在應該可以理解萬物皆對象這句話真正的含義了,但如果覺得這就是全部,那就太早了,這其實只是冰山一角,只是開始。

Oop-Klass模型

在上文中我們對Oop和Klass都有了最基本的認識,Oop用於描述對象,Klass用於描述類,而經過筆者更深入的學習中發現,在JVM中,情況絕不止第一節中提到的這麼簡單。

在JVM中,並沒有根據Java實例對象直接通過虛擬機映射到新建的C++對象,而是定義了各種Oop-Klass:

  • Oop(ordinary  object  pointer),用來描述對象實例資訊。
  • Klass,用來描述 Java 類,是虛擬機內部Java類型結構的對等體 。

而剛才我們看到的InstanceKlass,實際上只是Klass的一種。

Oop體系

看到Oop,大家第一反應一定是Object-oriented programming(面向對象程式設計),但是這裡的Oop,是值Ordinary Object Pointer,即標準對象指針,它用來表示對象的實例資訊。

在JVM源碼里,oopsHierarchy.hpp中定義了oop和klass各自的體系,這個是Oop的體系:

  typedef  class oopDesc*                               oop;//所有oops共同基類
  typedef  class   instanceOopDesc*              instanceOop;//Java類實例對象
  typedef  class methodOopDesc*        methodOop;//Java方法對象
  typedef  class constMethodOopDesc*     constMethodOop;//方法中的只讀資訊對象
  typedef  class methodDataOopDesc*      methodDataOop;//方法性能統計對象
  typedef  class   arrayOopDesc*                    arrayOop;//描述數組
  typedef  class   objArrayOopDesc*              objArrayOop;//描述引用數據類型數組
  typedef  class   typeArrayOopDesc*             typeArrayOop;//描述基本數據類型數組
  typedef  class constantPoolOopDesc*   constantPoolOop;//class文件中的常量池
  typedef  class constantPoolCacheOopDesc*  constantPoolCacheOop;//常量池快取
  typedef  class klassOopDesc*            klassOop;//指向klass實例
  typedef  class markOopDesc*       markOop;//對象頭
  typedef  class compiledICHolderOopDesc* compiledICHolderOop;

 

為了簡化變數名,JVM統一將結尾的Desc去掉,以Oop為結尾命名。

在Oop體系中,分別使用不同的Oop來表示不同的對象,在程式碼的注釋中,筆者已經註明了每一種oop分別用於表示什麼對象。HotSpot認為用這些模型,便足以描述Java程式的全部內容。

Klass體系

在JVM源碼里,oopsHierarchy.hpp中定義了oop和klass各自的體系,這個是Klass的體系:

  class                        Klass;//klass家族的基類
  class                InstanceKlass;//虛擬機層面與Java類對等的數據結構
  class          InstanceMirrorKlass;//描述java.lang.Class的實例
  class     InstanceClassLoaderKlass;//描述類載入器的實例
  class             InstanceRefKlass;//描述java.lang.Reference的子類
  class                  MethodKlass;//表示Java類中的方法
  class          ConstantMethodKlass;//描述Java類方法所對應的位元組碼指令資訊的固有屬性
  class                   KlassKlass;//Klass鏈路的末端,在Jdk8已不存在
  class               ConstPoolKlass;//描述位元組碼文件中常量池的屬性
  class                   ArrayKlass;//描述數組的資訊,是抽象類。
  class                ObjArrayKlass;//ArrayKlass的子類,描述引用類型的數組類元資訊
  class               TypeArrayKlass;//ArrayKlass的子類,描述普通配型的數組類元資訊

 

Klass主要提供一下兩種能力:

  • klass提供一個與 Java 類對等的 C++類型描述。
  • klass提供虛擬機內部的函數分發機制 。

由於在JVM中,Java類是以Oop和Klass分別進行表示的,所以Klass體系基本和Oop體系相互對應。

或許將兩個維度分開,對於我們真正理解這個體系並不是一件好事,因為畢竟這兩個體系息息相關,所以筆者在這裡只是淺嘗輒止地介紹了一下兩個體系的成員,接下來我們就以一個最簡單的案例來一步步了解Oop-Klass體系,順便驗證我們上文中所說的一些內容。根據上文提到的Oop體系和Klass體系內容,我們分別在Main方法中創建幾個對象:

public class Blog {
    private int a = 10;
    private int b = 20;
    public static void main(String[] args) {
        Blog blog = new Blog();
        int[] typeArray = new int[10];
        Integer[] objArray = new Integer[10];
        while(true){}
    }
}

 

按照我們上文的說法,Klass存儲類的元資訊,Oop用於描述對象的實例資訊,而我們都知道創建一個對象JVM一般分為三步,首先是在堆中先分配一片記憶體空間,第二步需要完成對象的初始化,最後將對象的引用指向該記憶體空間,當然這只是比較宏觀的一種說法,而落實到細節中,大概是這樣一個流程:

1.將Java類載入到方法區,載入到方法區的時候實際上就是創建了一個Klass,Klass中保存了這個Java類的所有資訊,例如:變數、方法、父類、介面、構造方法、屬性等。

2.而在完成對象的初始化時,JVM會在堆分配的空間中,創建一個Oop,這個Oop便是我們這個對象實例在記憶體中的對等體,主要存儲這個對象實例的成員變數,其中這個Oop中存在一個指針,指向Klass,通過這個指針,JVM可以在運行期間,獲取這個對象的所有類元資訊。

看到這裡可能有人會說,「哎呀這些不過是你說的,但是我們並沒有真正看過啊,你怎麼知道你說的這些就是對的呢?」。不急,我們依舊可以使用HSDB來驗證我們的說法。

還是上文的程式碼,打開HSDB後,找到我們創建的Blog對象:

類和對象在JVM中是如何存儲的,竟然有一半人回答不上來!驗證Oop內部

可以看到,我們創建的這個對象,其是由Oop所描述,而Oop對象中存在一個指向Klass的指針,指向Klass,並且Oop對象中主要存放了對象實例的成員變數,說明剛才我們的結論是正確的,而在「宏觀說法」中,對象的引用指向該記憶體空間,實際上就是指向這個Oop對象。那麼就可以根據這個操作結果,用一張圖來描述出Oop-Klass模型基本的樣子:

類和對象在JVM中是如何存儲的,竟然有一半人回答不上來!Oop-Klass模型圖

而左側Oop對象圖,實際上就是我們平常經常背的一道面試題的來源,Java對象由什麼組成:對象頭、實例數據、對齊填充,在這部分內容中,指向klass的指針還存在是否指針壓縮的概念。當然,這不是今天的重點,這部分內容我會在之後的JVM內容中作為單獨一篇文章來描述。

我們接著往下說,剛才我們只是證明了Oop和Klass模型的內部結構,以及Oop-Klass存在的聯繫,是通過一個指針關聯的,還有一個東西並沒有得以證明,就是在最初介紹Oop模型和Klass模型時,我們說過其家族的龐大,對於每一種不同類型的類和對象,都由不同的Oop及Klass進行描述,首先修改一下剛才的程式碼,使用HSDB來分別查看不同的類和對象,觀察其區別:

public class Blog {
    //基本數據類型
    private int a = 10;
    private int b = 20;
    //基本數據類型數組
    private int[]aArray = new int[10];
    //引用數據類型數組
    private Integer[] bArray = new Integer[10];
    //普通對象
    private Map<String,Object> mapObj = new HashMap<>(16);


    public static void main(String[] args) {
        Blog blog = new Blog();
        int[] typeArray = new int[10];
        Integer[] objArray = new Integer[10];
        while(true){}
    }
}

 

HSDB:

  1. 基本數據類型數組:

    類和對象在JVM中是如何存儲的,竟然有一半人回答不上來!基本數據類型數組

  2. 引用數據類型數組:

    類和對象在JVM中是如何存儲的,竟然有一半人回答不上來!引用數據類型數組

  3. 對象:

    類和對象在JVM中是如何存儲的,竟然有一半人回答不上來!對象

觀察HSDB,不難看出我們在Blog類中創建的三種不同類型的成員屬性:基本數據類型數組引用數據類型數組普通對象,都由不同的Oop-Klass模型進行表示,表示方式大致可以用下圖進行描述:

類和對象在JVM中是如何存儲的,竟然有一半人回答不上來!OopKlass表示類型

Oop-Klass模型的簡易理解

在JVM中,使用Oop-Klass模型這種一分為二的模型區描述Java類,但是筆者認為這種叫法並不是特別容易讓人理解,對於初學者來說,什麼是Oop,什麼是Klass?並沒有一種可以顧名思義的解讀,實際上,無非就是元數據和實例數據進行分離,所以初學者看到這裡,不妨可以把他直接理解為data-meta模型,data即oop、而meta即klass,這樣就可以很好地理解Oop-Klass這個概念了。

而實際上,在JVM中,Klass保存元數據這個概念會更好理解一些,如果你看過JVM源碼,你會發現,實際上在JVM源碼中Klass正是繼承Metadata類的。

結語

本文帶大家了解了Java的類與對象在JVM中的存在形式,JVM將其一分為二,分為Oop-Klass,分別存儲對象示例資訊及類的元資訊,在整個證明過程中,我們使用了HSDB這個強大的工具,對這一結構進行窺探及證明。

當然,Oop-Klass模型內部是一個龐大的體系,本文只是抓取了日常使用頻次比較高的類以及比較有特點的一些類進行驗證,感興趣的同學可以在線下根據這套方法,自己去驗證其他的一些類型的表示形式。

這是整個JVM專題的第一篇文章,關於JVM的更多內容將會在之後的JVM文章中進行分享。

如果需要提問,歡迎評論區留言~