HotSpot源码分析之C++对象的内存布局

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源码剖析系列文章!

<span>HotSpot源码分析之C++对象的内存布局</span>   

 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/2671.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 如何下载pycharm以前的版本_pycharm怎么下载包

    如何下载pycharm以前的版本_pycharm怎么下载包地址:https://www.jetbrains.com/pycharm/download/previous.html点击进去就能看到各个版本的下载链接。————————————————版权声明:本文为CSDN博主「lyj156」的原创文章,遵循CC4.0BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/lyj156/article/details/95887590…

  • linux命令行移动文件_centos移动文件到指定目录

    linux命令行移动文件_centos移动文件到指定目录在当前文件夹下打开命令行,输入cp文件名路径验证已经移动过去cd路径lltip:写文件名时可以先写开头几个字母,然后使用ctrl+tab补充完整文件名

  • 傅里叶级数与傅里叶变换公式推导「建议收藏」

    傅里叶级数与傅里叶变换公式推导「建议收藏」首先,傅里叶分析是指把一个周期或非周期函数展开成一个个三角函数的叠加,如果是对其还没有基本概念的,可以看看傅里叶分析之掐死教程,这篇文章不依赖数学公式却又十分透彻地讲述了傅里叶分析的基本概念,十分值得一读。但如果先深入探讨其中的数学由来,接下来会讲述详细的数学推导。傅里叶级数三角函数系的正交性三角函数系:{1,sinx,cosx,sin2x,cos2x,…,sinnx,cosnx,…},它由无数个sinnx和cosnx组成,其中n=0,1,2,…。正交性:∫−ππsin⁡nxcos⁡mxdx=0,

  • 原生sql和 TP sql怎么关联?

    原生sql和 TP sql怎么关联?

    2021年10月23日
  • git clone 慢

    git clone 慢首先:查询最新的DNS网上很多设置的IP可能不一样http://github.global.ssl.fastly.net.ipaddress.com/ 2.修改host文件:host文件在Windows的路径为C:\Windows\System32\drivers\etc最好先备份一个原文件(备份是好习惯)有记事本打开即可151.101.185.194github.global….

  • pycharm激活码2021年(破解版激活)

    pycharm激活码2021年(破解版激活),https://javaforall.cn/100143.html。详细ieda激活码不妨到全栈程序员必看教程网一起来了解一下吧!

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号