原创

HotSpot的二分模型

HotSpot采用了OOP-Klass模型,用来描述Java类和对象的一种模型。OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象的具体类型。

那么为何要设计这样一个一分为二的对象模型呢?这是因为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时使用就够了,更多关于内存布局的知识请参考其它文章或书籍。

现在我们回过头来讨论一下,为什么HotSpot要设计二分模型来表示Java的类和对象呢?假设我们不采用二分模式,那么C++类即要描述Java的类元信息,又要描述每个对象的特性,无可避免地会使用到C++的虚函数来完成相应功能,使得一个类如果创建大量对象,不但虚函数表会占据空间,类元信息也会随着对象占用空间,而这些信息是不会随着Java对象改变的。

假设定义一个类ClassAndObjectDesc用来描述Java类和对象:

class ClassAndObjectDesc{ // 描述Java类和对象

   Var vars[];  // 描述VarMetaData和VarValue

   MethodMetadata methods[];

   // 提供各种操作的虚函数或非虚函数

}

其中用Var数组来保存Java类中定义的全局变量,包括变量定义的信息和值,Method用来保存Java方法定义的信息。Java每创建一个对象,ClassAndObjectDesc就需要创建一个对象来描述,虚函数表会占用空间,类元信息也会占用空间。

正确的设计类似于这样:

class ClassDesc{ // 描述Java类

  VarMetaData var[];

  MethodMetadata methos[];

 // 提供各种操作的虚函数或非虚函数 
}

class ObjectDesc{  // 描述Java对象

 ClassDesc* classdesc;  // Java对象对应的类型

 VarValue var_vals[]; 

 // 只提供非虚函数
}

描述Java对象的类是不是清爽了很多,只不过有些操作要先通过classdesc找到Java对象对应的类型,然后在ClassDesc中操作。  

正文到此结束