重磅硬核 | 一文聊透对象在JVM中的内存布局(四)
5. 内存对齐
通过以上内容我们了解到Java对象中的实例数据区字段需要进行内存对齐而导致在JVM中会被重排列以及通过填充缓存行避免false sharding的目的所带来的字节对齐填充。
我们也了解到内存对齐不仅发生在对象与对象之间,也发生在对象中的字段之间。
那么在本小节中笔者将为大家介绍什么是内存对齐,在本节的内容开始之前笔者先来抛出两个问题:
• 为什么要进行内存对齐?如果就是头比较铁,就是不内存对齐,会产生什么样的后果?
• Java 虚拟机堆中对象的起始地址为什么需要对齐至 8的倍数?为什么不对齐至4的倍数或16的倍数或32的倍数呢?
带着这两个问题,下面我们正式开始本节的内容~~~
5.1 内存结构
我们平时所称的内存也叫随机访问存储器(random-access memory)也叫RAM。而RAM分为两类:
• 一类是静态RAM(SRAM),这类SRAM用于前边介绍的CPU高速缓存L1Cache,L2Cache,L3Cache。其特点是访问速度快,访问速度为1 - 30个时钟周期,但是容量小,造价高。
• 另一类则是动态RAM(DRAM),这类DRAM用于我们常说的主存上,其特点的是访问速度慢(相对高速缓存),访问速度为50 - 200个时钟周期,但是容量大,造价便宜些(相对高速缓存)。
内存由一个一个的存储器模块(memory module)组成,它们插在主板的扩展槽上。常见的存储器模块通常以64位为单位(8个字节)传输数据到存储控制器上或者从存储控制器传出数据。
image.png
如图所示内存条上黑色的元器件就是存储器模块(memory module)。多个存储器模块连接到存储控制器上,就聚合成了主存。
内存结构.png
而前边介绍到的DRAM芯片就包装在存储器模块中,每个存储器模块中包含8个DRAM芯片,依次编号为0 - 7。
存储器模块.png
而每一个DRAM芯片的存储结构是一个二维矩阵,二维矩阵中存储的元素我们称为超单元(supercell),每个supercell大小为一个字节(8 bit)。每个supercell都由一个坐标地址(i,j)。
i表示二维矩阵中的行地址,在计算机中行地址称为RAS(row access strobe,行访问选通脉冲)。j表示二维矩阵中的列地址,在计算机中列地址称为CAS(column access strobe,列访问选通脉冲)。
下图中的supercell的RAS = 2,CAS = 2。
DRAM结构.png
DRAM芯片中的信息通过引脚流入流出DRAM芯片。每个引脚携带1 bit的信号。
图中DRAM芯片包含了两个地址引脚(addr),因为我们要通过RAS,CAS来定位要获取的supercell。还有8个数据引脚(data),因为DRAM芯片的IO单位为一个字节(8 bit),所以需要8个data引脚从DRAM芯片传入传出数据。
注意这里只是为了解释地址引脚和数据引脚的概念,实际硬件中的引脚数量是不一定的。
5.2 DRAM芯片的访问
我们现在就以读取上图中坐标地址为(2,2)的supercell为例,来说明访问DRAM芯片的过程。
DRAM芯片访问.png
1.首先存储控制器将行地址RAS = 2通过地址引脚发送给DRAM芯片。
2.DRAM芯片根据RAS = 2将二维矩阵中的第二行的全部内容拷贝到内部行缓冲区中。
3.接下来存储控制器会通过地址引脚发送CAS = 2到DRAM芯片中。
4.DRAM芯片从内部行缓冲区中根据CAS = 2拷贝出第二列的supercell并通过数据引脚发送给存储控制器。
DRAM芯片的IO单位为一个supercell,也就是一个字节(8 bit)。
5.3 CPU如何读写主存
前边我们介绍了内存的物理结构,以及如何访问内存中的DRAM芯片获取supercell中存储的数据(一个字节)。
本小节我们来介绍下CPU是如何访问内存的。
CPU与内存之间的总线结构.png
其中关于CPU芯片的内部结构我们在介绍false sharding的时候已经详细的介绍过了,这里我们主要聚焦在CPU与内存之间的总线架构上。
5.3.1 总线结构
CPU与内存之间的数据交互是通过总线(bus)完成的,而数据在总线上的传送是通过一系列的步骤完成的,这些步骤称为总线事务(bus transaction)。
其中数据从内存传送到CPU称之为读事务(read transaction),数据从CPU传送到内存称之为写事务(write transaction)。
总线上传输的信号包括:地址信号,数据信号,控制信号。其中控制总线上传输的控制信号可以同步事务,并能够标识出当前正在被执行的事务信息:
• 当前这个事务是到内存的?还是到磁盘的?或者是到其他IO设备的?
• 这个事务是读还是写?
• 总线上传输的地址信号(内存地址),还是数据信号(数据)?。
还记得我们前边讲到的MESI缓存一致性协议吗?当core0修改字段a的值时,其他CPU核心会在总线上嗅探字段a的内存地址,如果嗅探到总线上出现字段a的内存地址,说明有人在修改字段a,这样其他CPU核心就会失效自己缓存字段a所在的cache line。
如上图所示,其中系统总线是连接CPU与IO bridge的,存储总线是来连接IO bridge和主存的。
IO bridge负责将系统总线上的电子信号转换成存储总线上的电子信号。IO bridge也会将系统总线和存储总线连接到IO总线(磁盘等IO设备)上。这里我们看到IO bridge其实起的作用就是转换不同总线上的电子信号。
5.3.2 CPU从内存读取数据过程
假设CPU现在要将内存地址为A的内容加载到寄存器中进行运算。
CPU读取内存.png
首先CPU芯片中的总线接口会在总线上发起读事务(read transaction)。该读事务分为以下步骤进行:
1.CPU将内存地址A放到系统总线上。随后IO bridge将信号传递到存储总线上。
2.主存感受到存储总线上的地址信号并通过存储控制器将存储总线上的内存地址A读取出来。
3.存储控制器通过内存地址A定位到具体的存储器模块,从DRAM芯片中取出内存地址A对应的数据X。
4.存储控制器将读取到的数据X放到存储总线上,随后IO bridge将存储总线上的数据信号转换为系统总线上的数据信号,然后继续沿着系统总线传递。
5.CPU芯片感受到系统总线上的数据信号,将数据从系统总线上读取出来并拷贝到寄存器中。
以上就是CPU读取内存数据到寄存器中的完整过程。
但是其中还涉及到一个重要的过程,这里我们还是需要摊开来介绍一下,那就是存储控制器如何通过内存地址A从主存中读取出对应的数据X的?
接下来我们结合前边介绍的内存结构以及从DRAM芯片读取数据的过程,来总体介绍下如何从主存中读取数据。
5.3.3 如何根据内存地址从主存中读取数据
前边介绍到,当主存中的存储控制器感受到了存储总线上的地址信号时,会将内存地址从存储总线上读取出来。
随后会通过内存地址定位到具体的存储器模块。还记得内存结构中的存储器模块吗??
内存结构.png
而每个存储器模块中包含了8个DRAM芯片,编号从0 - 7。
存储器模块.png
存储控制器会将内存地址转换为DRAM芯片中supercell在二维矩阵中的坐标地址(RAS,CAS)。并将这个坐标地址发送给对应的存储器模块。随后存储器模块会将RAS和CAS广播到存储器模块中的所有DRAM芯片。依次通过(RAS,CAS)从DRAM0到DRAM7读取到相应的supercell。
DRAM芯片访问.png
我们知道一个supercell存储了8 bit数据,这里我们从DRAM0到DRAM7 依次读取到了8个supercell也就是8个字节,然后将这8个字节返回给存储控制器,由存储控制器将数据放到存储总线上。
CPU总是以word size为单位从内存中读取数据,在64位处理器中的word size为8个字节。64位的内存也只能每次吞吐8个字节。
CPU每次会向内存读写一个cache line大小的数据(64个字节),但是内存一次只能吞吐8个字节。
所以在内存地址对应的存储器模块中,DRAM0芯片存储第一个低位字节(supercell),DRAM1芯片存储第二个字节,......依次类推DRAM7芯片存储最后一个高位字节。
内存一次读取和写入的单位是8个字节。而且在程序员眼里连续的内存地址实际上在物理上是不连续的。因为这连续的8个字节其实是存储于不同的DRAM芯片上的。每个DRAM芯片存储一个字节(supercell)。
读取存储器模块数据.png
5.3.4 CPU向内存写入数据过程
我们现在假设CPU要将寄存器中的数据X写到内存地址A中。同样的道理,CPU芯片中的总线接口会向总线发起写事务(write transaction)。写事务步骤如下:
1.CPU将要写入的内存地址A放入系统总线上。
2.通过IO bridge的信号转换,将内存地址A传递到存储总线上。
3.存储控制器感受到存储总线上的地址信号,将内存地址A从存储总线上读取出来,并等待数据的到达。
4.CPU将寄存器中的数据拷贝到系统总线上,通过IO bridge的信号转换,将数据传递到存储总线上。
5.存储控制器感受到存储总线上的数据信号,将数据从存储总线上读取出来。
6.存储控制器通过内存地址A定位到具体的存储器模块,最后将数据写入存储器模块中的8个DRAM芯片中。