HotSpot源碼分析之C++對象的內存布局


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的源代碼(配視頻)  

2、調試HotSpot源代碼(配視頻)

3、HotSpot項目結構

4、HotSpot的啟動過程(配視頻進行源碼分析)

搭建過程中如果有問題可直接評論留言或加作者微信mazhimazh。

作者持續維護的個人博客  classloading.com

B站上有HotSpot源碼分析相關視頻 https://space.bilibili.com/27533329

關注公眾號,有HotSpot源碼剖析系列文章!

   

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM