【JVM】Java对象的内存布局、对齐填充与指针压缩

目录

Java对象的内存布局

对象头(Header)

实例数据(Instance Data)

内存模型设计

内存模型设计之–指针压缩

指针压缩的目的

内存模型设计之–对齐填充

对齐填充的应用

什么是伪共享问题?

CPU缓存


Java对象的内存布局

对象头(Header)

每个Java对象都包含一个对象头,对象头中包含了两类信息:

  • MarkWord:在JVM中用markOopDesc 结构表示用于存储对象自身运行时的数据。比如:hashcode,GC分代年龄,锁状态标志,线程持有的锁,偏向线程Id,偏向时间戳等。在32位操作系统和64位操作系统中MarkWord分别占用4B和8B大小的内存。
  • 类型指针:JVM中的类型指针封装在klassOopDesc 结构中,类型指针指向了InstanceKclass对象,Java类在JVM中是用InstanceKclass对象封装的,里边包含了Java类的元信息,比如:继承结构,方法,静态变量,构造函数等。
    • 在不开启指针压缩的情况下(-XX:-UseCompressedOops)。在32位操作系统和64位操作系统中类型指针分别占用4B和8B大小的内存。
    • 在开启指针压缩的情况下(-XX:+UseCompressedOops)。在32位操作系统和64位操作系统中类型指针分别占用4B和4B大小的内存。
  • 如果Java对象是一个数组类型的话,那么在数组对象的对象头中还会包含一个4B大小的用于记录数组长度的属性。

由于在对象头中用于记录数组长度大小的属性只占4B的内存,所以Java数组可以申请的最大长度为:2^32。

为什么age>15 就需要从edn区到old区?因为markword中对象头存放分代年龄只有4个2进制位。

Mark Word的32个比特存储空间中的**25个比特用于存储对象哈希码,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,1个比特固定为0。

实例数据(Instance Data)

Java对象中的字段类型分为两大类:

  • 基础类型:Java类中实例字段定义的基础类型在实例数据区的内存占用如下:
    • long | double占用8个字节。
    • int | float占用4个字节。
    • short | char占用2个字节。
    • byte | boolean占用1个字节。
  • 引用类型:Java类中实例字段的引用类型在实例数据区内存占用分为两种情况:
    • 不开启指针压缩(-XX:-UseCompressedOops):在32位操作系统中引用类型的内存占用为4个字节。在64位操作系统中引用类型的内存占用为8个字节。
    • 开启指针压缩(-XX:+UseCompressedOops):在64为操作系统下,引用类型内存占用则变为为4个字节,32位操作系统中引用类型的内存占用继续为4个字节。

为什么32位操作系统的引用类型占4个字节,而64位操作系统引用类型占8字节?

在Java中,引用类型所保存的是被引用对象的内存地址。在32位操作系统中内存地址是由32个bit表示,因此需要4个字节来记录内存地址,能够记录的虚拟地址空间是2^32大小,也就是只能够表示4G大小的内存。

而在64位操作系统中内存地址是由64个bit表示,因此需要8个字节来记录内存地址,但在 64 位系统里只使用了低 48 位,所以它的虚拟地址空间是 2^48大小,能够表示256T大小的内存,其中低 128T 的空间划分为用户空间,高 128T 划分为内核空间,可以说是非常大了。

内存模型设计

内存模型设计之–指针压缩

指针压缩的目的

1. 为了保证CPU普通对象指针(oop)缓存

2. 为了减少GC的发生,因为指针不压缩是8字节,这样在64位操作系统的堆上其他资源空间就少了。

64位操作系统中 内存 > 4G 默认开启指针压缩技术,内存< 4G,默认是32位系统默认不开启。内存 > 32G 指针压缩失效。所以我们通常在部署服务时,JVM内存不要超过32G,因为超过32G就无法开启 指针压缩了。

在Java中,引用类型所保存的是被引用对象的内存地址。在32位操作系统中内存地址是由32个bit表示,因此需要4个字节来记录内存地址,能够记录的虚拟地址空间是2^32大小,也就是只能够表示4G大小的内存。而在64位操作系统中内存地址是由64个bit表示,因此需要8个字节来记录内存地址。(但在 64 位系统里只使用了低 48 位,所以它的虚拟地址空间是 2^48大小,能够表示256T大小的内存,其中低 128T 的空间划分为用户空间,高 128T 划分为内核空间,可以说是非常大了。)

引用类型8个字节来在内存中寻址会比较慢,造成性能的消耗,留给其他数据的内存减少,会加快gc的发生,降低cpu的缓存命中率

jdk1.6之后默认使用指针压缩

为什么要进行指针压缩?

如果我们是64位的操作系统,在64位操作系统中 jvm hotspot对象的应用(以及类型指针)占用64bit(位)也就是8个字节,

这就导致在64位操作系统的对象引用占用的内存空间是32位系统中的两倍大小,这样就会造成更多的内存消耗以及更频繁的GC发生,

GC占用的CPU时间越多,那么我们的应用程序占用CPU的时间就越少。

内存模型设计之–对齐填充

 由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

总的一句话来说,“数据项仅仅能存储在地址是数据项大小的整数倍的内存位置上(分别为偶地址、被4整除的地址、被8整除的地址)”比如int类型占用4个字节,地址仅仅能在0,4,8等位置上。

64位操作系统一次读取8个字节的数据,对齐填充空间换时间。

对齐填充的意义是 提高CPU访问数据的效率 ,主要针对会存在该实例对象数据跨内存地址区域存储的情况。

对齐填充到8字节的一个常见原因是与CPU的读取机制有关。许多现代的CPU架构在读取内存时会以缓存行(Cache Line)为单位进行操作,而缓存行的大小通常是8字节或16字节。

当CPU从内存中读取数据时,会一次性读取一个缓存行的内容并加载到CPU缓存中。如果需要的数据跨越了多个缓存行,CPU就需要多次读取内存,这会导致额外的延迟和性能损失。

通过将数据进行对齐填充,使得每个数据项占据一个缓存行(或多个缓存行的整数倍),可以最大程度地利用CPU的读取机制。例如,将4字节的数据进行对齐填充到8字节,确保每个数据项都占据一个缓存行,这样CPU在读取数据时就可以一次性读取整个缓存行,提高了读取效率。

需要注意的是,对齐填充的大小并不一定非要是CPU缓存行的大小,它可以根据具体的情况进行调整。在实际应用中,对齐填充的大小通常是根据性能测试和分析来确定的,以取得最佳的性能表现。

总结:将数据进行对齐填充到8字节的一个常见原因是与CPU的读取机制有关。CPU通常以缓存行为单位进行内存读取,通过对齐填充,可以最大程度地利用CPU的读取机制,提高读取效率。对齐填充的大小可以根据具体情况进行调整,以实现最佳的性能表现。

对齐填充的应用

解决伪共享问题带来的对齐填充。

提高cpu的读取效率,如果需要的数据跨越了多个缓存行,CPU就需要多次读取内存,这会导致额外的延迟和性能损失。

什么是伪共享问题?

假设内存中有a和b两个int类型的变量,没有对齐填充时,A线程只想要读取a,但a只占4字节,因此会连带b也会一并读取到缓存行中;而同一时刻B线程只想要读取b,但b只占4字节,因此会连带a也会一并读取到缓存行中;那么这种情况下就会存在伪共享问题,这会导致A和B线程在进行数据操作时会频繁地直接去内存获取最新值,而不是从高速缓存中获取(也就是缓存失效),导致运算速度下降。

而进行对齐填充后,A线程在读取缓存行数据时,原本a只是占4字节,那么此时就会填充4字节以达到8字节地倍数,这样读取的缓存行数据就是会存在a数据,B线程读取的缓存行数据只有b数据,这是空间换取时间,解决缓存行失效问题(伪共享问题)。

如何拒绝伪共享问题?

通过注解进行填充@Contended,sun.misc.Contended注解用来解决伪共享问题。

@sun.misc.Contended public final static class FilledLong { public volatile long value = 0L; }

CPU缓存

根据摩尔定律:芯片中的晶体管数量每隔

18个月就会翻一番。导致CPU的性能和处理速度变得越来越快,而提升CPU的运行速度比提升内存的运行速度要容易和便宜的多,所以就导致了CPU与内存之间的速度差距越来越大。

CPU 比如是4核心8线程 4个核心表示同一个时间运4个线程并行执行,8个逻辑核心表示处理器利用超线程的技术将一个物理核心模拟出了两个逻辑核心,一个物理核心在同一时间只会执行一个线程。

​实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在Java源码中定义顺序的影响。

​HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops。

​从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。

​对象的第三部分是对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。

引用blog:

JVM 的指针压缩_jvm指针压缩-优快云博客

指针压缩实现原理_java指针压缩原理-优快云博客

JVM对象创建和对齐填充详解_对象未被同步锁锁定的状态下”“1 个比特固定为 0”是否就是为了进行对齐填充?-优快云博客

对齐填充的目的-优快云博客

https://www.cnblogs.com/krock/p/14337544.html

Java中的伪共享问题-优快云博客

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值