HotSpot二分模型(1)

HotSpot採用了OOP-Klass模型來描述Java類和對象。OOP(Ordinary Object Pointer)指的是普通對象指針,而Klass用來描述對象的具體類型。

那麼爲何要設計這樣一個一分爲二的對象模型呢?因爲類和對象本來就不是一個概念,分別使用不同的對象模型描述符合軟件開發的設計思想。另外英文註釋也說明了其中的一個原因:

One reason for the oop/klass dichotomy in the implementation is that we don't want a C++ vtbl pointer in every object. Thus,
normal oops don't have any virtual functions. Instead, they forward all "virtual" functions to their klass, which does have
a vtbl and does the C++ dispatch depending on the object's actual type. (See oop.inline.hpp for some of the forwarding code.)

根據註釋描述,HotSopt的設計者不想讓每個對象中都含有一個vtable(虛函數表),所以就把對象模型拆成klass和oop,其中oop中不含有任何虛函數,而klass就含有虛函數表,可以進行方法分發。

我們簡單介紹一下C++中對象的內存佈局,這樣才能瞭解二分模型設計的原因。同時也要介紹一下關於C++中虛函數的分派,這樣在講解Java語言的多態時就不用再補這一塊的C++知識了。

下面分情況介紹C++對象的內存局部。

1、只含有數據成員的對象 

class Base1{
  
public:
int base1_var1;
int base1_var2;
 
};

通過在VS中配置/d1 reportSingleClassLayoutBase1命令來查看對象的內存佈局,如下:

1>  class Base1  size(8):
1>   +---
1>   0   | base1_var1
1>   4   | base1_var2
1>   +---

可以看到,成員變量是按照定義的順序來保存的,類對象的大小就是所有成員變量的大小之和。 

2、沒有虛函數的對象

class Base1{
  
public: 
int base1_var1; 
int base1_var2; 
  
void func(){}  
};

C++中有方法的動態分派,就類似於Java中方法的多態。而C++實現動態分派主要就是通過虛函數來完成的,非虛函數在編譯時就已經確定調用目標。C++中的虛函數通過關鍵字virtual來聲明,如上函數func()沒有virtual關鍵字,所以是非虛函數。  

查看內存佈局,如下:

1>  class Base1  size(8):
1>   +---
1>   0   | base1_var1
1>   4   | base1_var2
1>   +---

非虛函數不會影響內存佈局。 

3、含有虛函數的對象 

class Base1{
  
public: 
int base1_var1; 
int base1_var2; 
  
virtual void base1_fun1() {}
 
};

內存佈局如下:

1>  class Base1  size(16):
1>   +---
1>   0   | {vfptr}
1>   8   | base1_var1
1>  12   | base1_var2
1>   +---

在64位環境下,指針佔用8字節,而vfptr就是指向虛函數表(vtable)的指針,其類型爲void**, 這說明它是一個void*指針。類似於在類Base1中定義瞭如下類似的僞代碼:

void* vtable[1] = {  &Base1::base1_fun1  };
 
const void**  vfptr = &vtable[0];

另外我們還可以看到,虛函數指針vfptr位於所有的成員變量之前。 

我們在上面的例子中再添加一個虛函數,如下:

virtual void base1_fun2() {}

內存佈局如下:

1>  class Base1  size(16):
1>   +---
1>   0   | {vfptr}
1>   8   | base1_var1
1>  12   | base1_var2
1>   +---

可以看到,內存佈局無論有一個還是多個虛函數都是一樣的,改變的只是vfptr指向的虛函數表中的項。類似於在類Base1中定義瞭如下類似的僞代碼: 

void* vtable[] = { &Base1::base1_fun1, &Base1::base1_fun2 };
 
const void** vfptr = &vtable[0];

4、繼承類對象

class Base1{
  
public:
  
int base1_var1; 
int base1_var2;
  
  
virtual void base1_fun1() {} 
virtual void base1_fun2() {}
  
};
  
  
class Derive1 : public Base1{
  
public:
  
int derive1_var1; 
int derive1_var2;
  
};

通過在VS中配置/d1 reportSingleClassLayoutDerive1命令來查看Derive1對象的內存佈局,如下: 

1>  class Derive1    size(24):
1>   +---
1>   | +--- (base class Base1)
1>   0   | | {vfptr}
1>   8   | | base1_var1
1>  12   | | base1_var2
1>   | +---
1>  16   | derive1_var1
1>  20   | derive1_var2
1>   +---

可以看到,基類在上邊, 繼承類的成員在下邊,並且基類的內存佈局與之前介紹的一模一樣。繼續來改造如上的實例,爲派生類Derive1添加一個與基本base1_fun1()函數一模一樣的虛函數,如下:

class Base1{
  
public:
  
int base1_var1; 
int base1_var2;
  
  
virtual void base1_fun1() {} 
virtual void base1_fun2() {}
  
};
  
  
class Derive1 : public Base1{
  
public:
  
int derive1_var1; 
int derive1_var2;
 
virtual void base1_fun1() {} // 覆蓋基類函數
  
};

佈局如下:

1>  class Derive1    size(24):
1>   +---
1>   | +--- (base class Base1)
1>   0   | | {vfptr}
1>   8   | | base1_var1
1>  12   | | base1_var2
1>   | +---
1>  16   | derive1_var1
1>  20   | derive1_var2
1>   +---

基本的佈局沒變,不過由於發生了虛函數覆蓋,所以虛函數表中的內容已經發生了變化,類似於在類Derive1中定義瞭如下類似的僞代碼:  

void* vtable[] = { &Derive1::base1_fun1, &Base1::base1_fun2 };
 
const void** vfptr = &vtable[0];

可以看到,vtable[0]指針指向的是Derive1::base1_fun1()函數。所以當調用Derive1對象的base1_fun1()函數時,會根據虛函數表找到Derive1::base1_fun1()函數進行調用,而當調用Base1對象的base1_fun1()函數時,由於Base1對象的虛函數表中的vtable[0]指針指向Base1::base1_func1()函數,所以會調用Base1::base1_fun1()函數。是不是和Java中方法的多態很像?那麼HotSpot虛擬機是怎麼實現Java方法的多態呢?我們後續在講解Java方法時會詳細介紹。

下面繼續看虛函數的相關實例,如下:

class Base1{
  
public:
  
int base1_var1; 
int base1_var2;
  
  
virtual void base1_fun1() {} 
virtual void base1_fun2() {}
  
};
  
  
class Derive1 : public Base1{
  
public:
  
int derive1_var1; 
int derive1_var2;
 
virtual void derive1_fun1() {}
  
};

對象的內存佈局如下: 

1>  class Derive1    size(24):
1>   +---
1>   | +--- (base class Base1)
1>   0   | | {vfptr}
1>   8   | | base1_var1
1>  12   | | base1_var2
1>   | +---
1>  16   | derive1_var1
1>  20   | derive1_var2
1>   +---

對象的內存佈局沒有改變,改變的仍然是虛函數表,類似於在類Derive1中定義瞭如下類似的僞代碼:  

void* vtable[] = { &Derive1::base1_fun1, &Base1::base1_fun2,&Derive1::derive1_fun1 };
 
const void** vfptr = &vtable[0];

可以看到,在虛函數表中追加了&Derive1::derive1_fun1()函數。  

好了,關於對象的佈局我們就簡單的介紹到這裏,因爲畢竟不是在研究C++,只要夠我們研究HotSpot時使用就夠了,更多關於內存佈局的知識請參考其它文章或書籍。

相關文章的鏈接如下:

Ubuntu16.04上編譯OpenJDK8源代碼

調試HotSpot源代碼

HotSpot項目結構

HotSpot的啓動過程

關注個人博客www.classloading.com或公衆號,有HotSpot源碼剖析系列文章! 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章