重磅硬核 | 一文聊透对象在JVM中的内存布局(六)

r660926
发布于 2022-8-9 18:55
浏览
0收藏

8. 数组对象的内存布局


前边大量的篇幅我们都是在讨论Java普通对象在内存中的布局情况,最后这一小节我们再来说下Java中的数组对象在内存中是如何布局的。

 

8.1 基本类型数组的内存布局

重磅硬核 | 一文聊透对象在JVM中的内存布局(六)-鸿蒙开发者社区 基本类型数组内存布局.png


上图表示的是基本类型数组在内存中的布局,基本类型数组在JVM中用typeArrayOop结构体表示,基本类型数组类型元信息用TypeArrayKlass 结构体表示。

 

数组的内存布局大体上和普通对象的内存布局差不多,唯一不同的是在数组类型对象头中多出了4个字节用来表示数组长度的部分。

 

我们还是分别以开启指针压缩和关闭指针压缩两种情况,通过下面的例子来进行说明:

long[] longArrayLayout = new long[1];

开启指针压缩 -XX:+UseCompressedOops

重磅硬核 | 一文聊透对象在JVM中的内存布局(六)-鸿蒙开发者社区 image.png


我们看到红框部分即为数组类型对象头中多出来一个4字节大小用来表示数组长度的部分。

 

因为我们示例中的long型数组只有一个元素,所以实例数据区的大小只有8字节。如果我们示例中的long型数组变为两个元素,那么实例数据区的大小就会变为16字节,以此类推................。

 

关闭指针压缩  -XX:-UseCompressedOops

重磅硬核 | 一文聊透对象在JVM中的内存布局(六)-鸿蒙开发者社区 image.png


当关闭了指针压缩时,对象头中的MarkWord还是占用8个字节,但是类型指针从4个字节变为了8个字节。数组长度属性还是不变保持4个字节。

 

这里我们发现是实例数据区与对象头之间发生了对齐填充。大家还记得这是为什么吗??

 

我们前边在字段重排列小节介绍了三种字段排列规则在这里继续适用:

 

 • 规则1:如果一个字段占用X个字节,那么这个字段的偏移量OFFSET需要对齐至NX

 

 • 规则2:在开启了压缩指针的64位JVM中,Java类中的第一个字段的OFFSET需要对齐至4N,在关闭压缩指针的情况下类中第一个字段的OFFSET需要对齐至8N

 

这里基本数组类型的实例数据区中是long型,在关闭指针压缩的情况下,根据规则1和规则2需要对齐至8的倍数,所以要在其与对象头之间填充4个字节,达到内存对齐的目的,起始地址变为24

 

8.2 引用类型数组的内存布局

重磅硬核 | 一文聊透对象在JVM中的内存布局(六)-鸿蒙开发者社区 引用类型数组的内存布局.png


上图表示的是引用类型数组在内存中的布局,引用类型数组在JVM中用objArrayOop结构体表示,基本类型数组类型元信息用ObjArrayKlass 结构体表示。

 

同样在引用类型数组的对象头中也会有一个4字节大小用来表示数组长度的部分。

 

我们还是分别以开启指针压缩和关闭指针压缩两种情况,通过下面的例子来进行说明:

public class ReferenceArrayLayout {
    char a;
    int b;
    short c;
}

ReferenceArrayLayout[] referenceArrayLayout = new ReferenceArrayLayout[1];

开启指针压缩 -XX:+UseCompressedOops

重磅硬核 | 一文聊透对象在JVM中的内存布局(六)-鸿蒙开发者社区image.png


引用数组类型内存布局与基础数组类型内存布局最大的不同在于它们的实例数据区。由于开启了压缩指针,所以对象引用占用内存大小为4个字节,而我们示例中引用数组只包含一个引用元素,所以这里实例数据区中只有4个字节。相同的到道理,如果示例中的引用数组包含的元素变为两个引用元素,那么实例数据区就会变为8个字节,以此类推......。

 

最后由于Java对象需要内存对齐至8的倍数,所以在该引用数组的实例数据区后填充了4个字节。

 

关闭指针压缩 -XX:-UseCompressedOops

重磅硬核 | 一文聊透对象在JVM中的内存布局(六)-鸿蒙开发者社区 image.png


当关闭压缩指针时,对象引用占用内存大小变为了8个字节,所以引用数组类型的实例数据区占用了8个字节。

 

根据字段重排列规则2,在引用数组类型对象头与实例数据区中间需要填充4个字节以保证内存对齐的目的。

 
总结


本文笔者详细介绍了Java普通对象以及数组类型对象的内存布局,以及相关对象占用内存大小的计算方法。

 

以及在对象内存布局中的实例数据区字段重排列的三个重要规则。以及后边由字节的对齐填充引出来的false sharding问题,还有Java8为了解决false sharding而引入的@Contented注解的原理及使用方式。

 

为了讲清楚内存对齐的底层原理,笔者还花了大量的篇幅讲解了内存的物理结构以及CPU读写内存的完整过程。

 

最后又由内存对齐引出了压缩指针的工作原理。由此我们知道进行内存对齐的四个原因:

 

 • CPU访问性能:当CPU访问内存对齐的地址时,可以通过一个read transaction读取一个字长(word size)大小的数据出来。否则就需要两个read transaction。

 

 • 原子性:CPU可以原子地操作一个对齐的word size memory。
 

• 尽可能利用CPU缓存:内存对齐可以使对象或者字段尽可能的被分配到一个缓存行中,避免跨缓存行存储,导致CPU执行效率减半。

 

 • 提升压缩指针的内存寻址空间: 对象与对象之间的内存对齐,可以使我们在64位系统中利用32位对象引用将内存寻址空间提升至32G。既降低了对象引用的内存占用,又提升了内存寻址空间。


在本文中我们顺带还介绍了和内存布局相关的几个JVM参数:-XX:+UseCompressedOops, -XX +CompactFields ,-XX:-RestrictContended ,-XX:ContendedPaddingWidth, -XX:ObjectAlignmentInBytes。

 

最后感谢大家能看到这里,我们下篇文章再见~~~

标签
已于2022-8-9 18:55:25修改
收藏
回复
举报
回复
    相关推荐