1. 程式人生 > >類和物件在JVM中是如何儲存的,竟然有一半人回答不上來!

類和物件在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:

啟動HSDB

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

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

 

在終端中使用jps -l命令,檢視執行起來的Java程序的程序號。

jps檢視程序

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

HSDBAttach

點選【OK】,即可繫結程序,下圖中是這個Java程序中的所有執行緒。

繫結程序成功

檢視類

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

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

檢視類

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

檢視類資料

到這裡我們已經可以看到,我們所建立的類,其在記憶體中的存在形式,實際上是使用一個名為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中是怎樣的:

找到建立的物件:

Main執行緒堆疊內容找到執行緒堆疊中物件

可以看到在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物件:

驗證Oop內部

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

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. 基本資料型別陣列:

    基本資料型別陣列
  2. 引用資料型別陣列:

    引用資料型別陣列
  3. 物件:

    物件

觀察HSDB,不難看出我們在Blog類中建立的三種不同型別的成員屬性:基本資料型別陣列、引用資料型別陣列、普通物件,都由不同的Oop-Klass模型進行表示,表示方式大致可以用下圖進行描述:

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文章中進行分享。

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