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的源代码(配视频)
搭建过程中如果有问题可直接评论留言或加作者微信mazhimazh。
作者持续维护的个人博客 classloading.com。
关注公众号,有HotSpot源码剖析系列文章!
HotSpot源码分析之C++对象的内存布局的更多相关文章
- 源码分析:Java对象的内存分配
Java对象的分配,根据其过程,将其分为快速分配和慢速分配两种形式,其中快速分配使用无锁的指针碰撞技术在新生代的Eden区上进行分配,而慢速分配根据堆的实现方式.GC的实现方式.代的实现方式不同而具有 ...
- HotSpot源码分析之类模型
HotSpot采用了OOP-Klass模型描述Java的类和对象.Klass模型采用Klass类及相关子类的对象来描述具体的Java类.一般HotSpot JVM 在加载Java的Class 文件时, ...
- JVM源码分析之Java对象头实现
原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十一篇. 今天呢!灯塔君跟大家讲: JVM源码分析之Java对象头实现 HotSpot虚拟机中,对象在内存中的布局分为三 ...
- Spring AOP 源码分析 - 创建代理对象
1.简介 在上一篇文章中,我分析了 Spring 是如何为目标 bean 筛选合适的通知器的.现在通知器选好了,接下来就要通过代理的方式将通知器(Advisor)所持有的通知(Advice)织入到 b ...
- Flask框架 (四)—— 请求上下文源码分析、g对象、第三方插件(flask_session、flask_script、wtforms)、信号
Flask框架 (四)—— 请求上下文源码分析.g对象.第三方插件(flask_session.flask_script.wtforms).信号 目录 请求上下文源码分析.g对象.第三方插件(flas ...
- spark 源码分析之十六 -- Spark内存存储剖析
上篇spark 源码分析之十五 -- Spark内存管理剖析 讲解了Spark的内存管理机制,主要是MemoryManager的内容.跟Spark的内存管理机制最密切相关的就是内存存储,本篇文章主要介 ...
- Shiro源码分析之SecurityManager对象获取
目录 SecurityManager获取过程 1.SecurityManager接口介绍 2.SecurityManager实例化时序图 3.源码分析 4.总结 @ 上篇文章Shiro源码分析之获 ...
- [旧][Android] Retrofit 源码分析之 ServiceMethod 对象
备注 原发表于2016.05.03,资料已过时,仅作备份,谨慎参考 前言 大家好,我又来学习 Retrofit 了,可能这是最后一篇关于 Retrofit 框架的文章了.我发现源码分析这回事,当时看明 ...
- spark 源码分析之十五 -- Spark内存管理剖析
本篇文章主要剖析Spark的内存管理体系. 在上篇文章 spark 源码分析之十四 -- broadcast 是如何实现的?中对存储相关的内容没有做过多的剖析,下面计划先剖析Spark的内存机制,进而 ...
随机推荐
- Redis6 安装
在centos7.5服务器上按照官方发布的安装方式并不能进行正确的安装,现收集并整理如下安装方式,亲测有效 1.安装依赖 yum install -y cpp binutils glibc glibc ...
- spring boot: 通过filter过滤器实现中文的简体繁体字符集转换(spring boot 2.3.1)
一,为什么要使用filter来实现简繁体转换? 项目中有时会有同时支持简体和繁体两种字符集的要求, 或者搜索引擎有支持繁体输入字符的需求. 针对繁体字符的显示, 我们通常会在数据库和模板.文案配置中默 ...
- 第二十二章 Nginx性能优化
一.性能优化概述 1.我们需要了解 1.首先需要了解我们当前系统的结构和瓶颈,了解当前使用的是什么,运行的是什么业务,都有哪些服务,了解每个服务最大能支撑多少并发.比如nginx作为静态资源服务并发是 ...
- 如何高效定义和验证restful请求的参数
go-zero针对文本的序列化和反序列化主要在三个地方使用 http api请求体的反序列化 http api返回体的序列化 配置文件的反序列化 完整示例可参照下面这篇文章: 快速构建高并发微服务 1 ...
- Hive源码解析
date: 2020-07-08 15:12:00 updated: 2020-08-21 17:38:00 Hive源码解析 入口:hive-cli-1.1.0-cdh5.14.4.jar!/org ...
- 《JavaScript高级程序设计》——第四章 变量、作用域和内存管理
JavaScript变量可以用保存两种类型的值:基本类型值和引用类型值.基本类型的值源自以下基本类型数据:Undefined.Null.Boolean.Number和String. 从一个变量向另一个 ...
- Albert学习记录
albert相对BERT而言主要有三个改进方向: 1.对Embedding因式分解 在BERT中,词向量维度E和隐层维度H是相等的.而词嵌入学习的是单词与上下文无关的表示,而隐层则是学习与上下文相关的 ...
- Luogu P6815 [PA2009]Cakes
题意 给定一个 \(n\) 个点 \(m\) 条边的图,点 \(i\) 有权值 \(a_i\),一个三元环 \((i,j,k)\) 的贡献为 \(\max(a_i,a_j,a_k)\),求所有三元环的 ...
- DOM属性/节点属性
DOM属性:DOM(Document Object Model,文档对象模型)一种独立于语言,用于操作xml,html的应用编程接口1:获取节点: document.getElementById(id ...
- python网络编程(TCP/IP、发邮件)
TCP/IP 关注公众号"轻松学编程"了解更多. 计算机为了联网,就必须规定通讯协议,早期的计算机网络是由各个厂商规定的一些协议,他们之间互不兼容. 为了把全世界的电脑能够连接到一 ...