HotSpot采用了OOP-Klass模型來描述Java類和對象。OOP(Ordinary Object Pointer)指的是普通對象指針,而Klass用來描述對象的具體類型。為了更好理解這個模型,首先要介紹一下C++的內存對象模型和虛函數。
1、C++類對象的內存布局
我們使用Visual Studio工具來查看C++對象的內存布局,所以需要在當前項目上右鍵單擊選擇“屬性”后,打開屬性頁,在配置屬性->C/C++->命令行下的其它選項文本框中配置如下命令:
/d1 reportAllClassLayout
這樣,運行main()函數后就會打印出對應的內存布局。如果想要指定看某個類的內存布局時,可以配置命令:
/d1 reportSingleClassLayoutXXX // XXX表示類名
內存布局的原則,簡單來說就是:成員變量按其被聲明的順序排列,按具體實現所規定的對齊原則在內存地址上對齊。
class Base1{ public: char base1_var1; int base1_var2; static int base1_var3; void func(){} };
輸出的布局如下:
1> class Base1 size(8): 1> +--- 1> 0 | base1_var1 1> | <alignment member> (size=3) 1> 4 | base1_var2 1> +---
根據如上的布局結果可知:
(一)類內部的成員變量:
- 普通的變量要占用內存,按照聲明成員的先后順序進行布局(類內偏移從0開始),但是要注意對齊原則。對於如上實例來說,4個字節包含一個字符(實際占用1個字節,3個字節空着,補對齊),后4個字節包含一個整數。A的指針就指向字符開始字節處。
- static修飾的靜態變量不占用內容,原因是編譯器將其放在全局變量區。
(二)類內部的成員函數:
- 普通函數不占用內存。
- 虛函數要占用8個字節,用來指定虛擬函數表的入口地址。后面會介紹。
空類也會占用內存空間的,而且大小是1,原因是C++要求每個實例在內存中都有獨一無二的地址。
下面繼續討論有繼承的情況,如下:
class Base1{ public: char base1_var1; int base1_var2; static int base1_var3; void func(){} }; class Derived1:public Base1{ public: int derived1_var1; };
輸出的布局如下:
1> class Derived1 size(12): 1> +--- 1> | +--- (base class Base1) 1> 0 | | base1_var1 1> | | <alignment member> (size=3) 1> 4 | | base1_var2 1> | +--- 1> 8 | derived1_var1 1> +---
可以看到,子類繼承了父類的成員變量,在內存布局上,先是布局了父類的成員變量(父類的內存分布不變),接着布局子類的成員變量。
在HotSpot中,經常需要計算類本身需要占用的內在大小,只要通過sizeof來計算即可。編寫main() 函數來測試:
void main(int argc,char *argv[]){ cout << "Base1的大小" << sizeof(Base1) << endl; cout << "Derived1的大小" << sizeof(Derived1) << endl; system("pause"); // 為了讓運行程序停止,以便察看結果 }
運行后打印結果如下:
Base1的大小8
Derived1的大小12
另外在HotSpot中經常做的操作就是計算某個變量的偏移量。例如定義的用來表示Java類的C++類Klass中有如下2個函數:
static ByteSize access_flags_offset(){ return in_ByteSize(offset_of(Klass, _access_flags)); }
其中的_access_flags屬性就是定義在Klass中的,通過調用access_flags_offset()來計算這個屬性在類中的偏移量。offset_of是一個宏,如下:
#define offset_of(klass,field) (size_t)((intx)&(((klass*)16)->field) - 16)
則經過宏替換和格式調整后的方法如下:
static ByteSize access_flags_offset(){ return in_ByteSize((size_t)( (intx)&( ((Klass*)16)->_access_flags) - 16 )); }
通過(intx)&(((Klass*)16)->_access_flags) - 16 方式來計算出具體的偏移量。解釋一下這種寫法。
假如定義個變量Klass a; 我們都知道&a表示變量a的首地址,&(a._access_flags)表示變量_access_flags的地址,那么&(a._access_flags)減去&a就得到_access_flags的偏移量。
((Klass*)16)的地址為16,所以偏移量最終等於&( ((Klass*)16)->_access_flags)減去16。
當HotSpot JVM要用一個成員變量的時候,它會根據對象的首地址加上成員的偏移量得到成員變量的地址。當對象的首地址為0時,得到的成員變量地址就是它的偏移量。
2、虛函數
HotSpot采用了OOP-Klass模型來描述Java類和對象。那么為何要設計這樣一個一分為二的對象模型呢?因為類和對象本來就不是一個概念,分別使用不同的對象模型描述符合軟件開發的設計思想。另外英文注釋也說明了其中的一個原因:
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++中對象的內存布局的。
1、只含有數據成員的對象
class Base1{ public: int base1_var1; int base1_var2; };
對象的內存布局如下:
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; };
查看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時使用就夠了,更多關於內存布局的知識請參考其它文章或書籍。
其它文章:
1、在Ubuntu 16.04上編譯OpenJDK8的源代碼(配視頻)
搭建過程中如果有問題可直接評論留言或加作者微信mazhimazh。
作者持續維護的個人博客 classloading.com。
B站上有HotSpot源碼分析相關視頻 https://space.bilibili.com/27533329
關注公眾號,有HotSpot源碼剖析系列文章!