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時使用就夠了,更多關於內存佈局的知識請參考其它文章或書籍。
相關文章的鏈接如下:
關注個人博客www.classloading.com或公衆號,有HotSpot源碼剖析系列文章!