JVM指针压缩实现原理
JVM指针压缩实现原理
概要
Java 中的指针压缩(Pointer Compression)是一个与内存管理相关的优化技术,主要应用于 JVM 的对象引用(即指针)的存储方式。指针压缩的目标是减少对象引用占用的内存空间,从而提高内存利用效率,特别是在 64 位系统上。
一、对象的内存布局
在了解指针压缩之前,需要先搞懂java的实例对象在JVM虚拟机中内存结构是什么样的。
在Hotspot虚拟机中,对象在内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
1. 对象头(Header)
对象头,又包括三部分:MarkWord、元数据指针、数组长度。
64位系统下的对象头布局如下图:
1)MarkWord
用于存储对象运行时的数据,比如HashCode、锁状态标志、GC分代年龄等。这部分在64位操作系统下,占8字节(64bit),在32位操作系统下,占4字节(32bit)。
2)指针
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
这部分就涉及到一个指针压缩的概念,在开启指针压缩的情况下,占4字节(32bit),未开启情况下,占8字节(64bit),现在JVM在1.6之后,在64位操作系统下都是默认开启的。
3)数组长度:这部分只有是数组对象才有,如果是非数组对象,就没这部分了,这部分占4字节(32bit)。
2. 实例数据(Instance Data)
用于存储对象中的各种类型的字段信息(包括从父类继承来的)。
3. 对齐填充(Padding)
这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于java对象的大小默认是按照8字节对齐,也就是说java对象的大小必须是8字节的倍数。如果算到最后不够8字节的话,那么就会进行对齐填充。
如果对象头+对象体大小不是8字节的倍数,则通过该部分进行补齐,比如对象头+对象体大小只有30字节,则需要补齐到32字节,这里的对齐填充就是2字节。
二、指针压缩
1. 为什么要指针压缩?
计算机操作系统分32位和64位,这里的位在计算机里是用0和1来表示的,用32个(或64个)二进制0和1的组合来表示内存地址。以32位为例,在普通的内存中,对象的大小最小是以1字节来计算的,通过0和1的排列组合,能够表示寻址的内存空间最大就是2^32个,换算成内存空间就是2 ^ 32 / 1024 / 1024 / 1024 = 4G,也就是说32位的操作系统最大能寻址的内存空间只有4G。同理,64位的操作系统最大能寻址的内存空间就更大了。
但是当我们准备将32位系统切换到64位系统,起初我们可能会期望系统性能会立马得到提升,但现实情况可能并不是这样的,为什么呢?
1)增加了GC开销
64位对象引用需要占用更多的堆空间,留给其他数据的空间将会减少,从而加快了GC的发生,更频繁的进行GC。GC占用的CPU时间越多,那么我们的应用程序占用CPU的时间就越少,响应会变慢,吞吐量会降低。
2)降低CPU缓存命中率
64位对象引用增大了,CPU能缓存的对象将会更少,从而降低了CPU缓存的效率,增加了对内存的访问。CPU对CPU缓存的访问速度可比对内存的访问速度快太多了,所以大量的对内存访问,会降低CPU的执行效率,增加了执行时间,从而影响性能。
分析:既然32位系统内存不够,64位内存够但又影响性能,那有没有折中方案来解决这两个问题呢,于是聪明的JVM开发者想到了利用压缩指针,在64位的操作系统中利用32位的对象指针引用获得超过4G的内存寻址空间。
2. 如何压缩指针
在 JVM 中,对象的内存地址总是 8 字节对齐,即每个对象的地址都是 8 的倍数,这意味着其二进制表示的末尾 3 位始终为 0。指针压缩正是利用了这一特点,省略了这些无意义的 0 位:
1)压缩存储
使用 32 位来存储指针(而非 64 位),既然JVM已经知道了这些对象的内存地址后三位始终是0,那么这些无意义的0就没必要在堆中继续存储。相反,我们可以利用存储0的这3位bit存储一些有意义的信息,这样我们就多出3位bit的寻址空间,也就是说如果我们继续使用32位来存储指针,只不过后三位原本用来存储0的bit现在被我们用来存放有意义的地址空间信息。
2)地址还原
寻址的时候,JVM将这32位的对象引用左移3位即可(后三位补0)。我们原本32位的内存寻址空间一下变成了35位,可寻址的内存空间变为2 ^ 35 / 1024 / 1024 / 1024 = 32G,也就是说在64位系统JVM的内存可扩大到32G了,基本上可满足大部分应用的使用了。
所以在64位系统下,通过压缩指针我们可以继续使用32位来处理(引用指针由8字节可降低到4字节),存储的时候右移3位,寻址的时候左移3位。
如下图所示:
这样一来,JVM虽然额外的执行了一些位运算但是极大的提高了寻址空间,并且将对象引用占用内存大小降低了一半,节省了大量空间,况且这些位运算对于CPU来说是非常容易且轻量的操作,可谓是一举两得。
正是因为对象地址对齐至8的倍数,才会多出3位bit让我们存储额外的地址信息,进而将4G的寻址空间提升至32G。
这种方式看作是一种内存地址映射。java对象的指针地址就可以不用存对象的真实的64位地址了,而是可以存一个映射地址编号。
3. JVM指针压缩参数
关于压缩指针的两个参数:
UseCompressedClassPointers:压缩类指针
UseCompressedOops:压缩普通对象指针
Oops是Ordinary object pointers的缩写,这两个参数默认是开启的,即-XX:+UseCompressedClassPointers,-XX:+UseCompressedOops,也可手动设置,
如下所示:
-XX:+UseCompressedClassPointers //开启压缩类指针 -XX:-UseCompressedClassPointers //关闭压缩类指针 -XX:+UseCompressedOops //开启压缩普通对象指针 -XX:-UseCompressedOops //关闭压缩普通对象指针
注意:32位HotSpot VM是不支持UseCompressedOops参数的,只有64位HotSpot VM才支持。Oracle JDK从6 update 23开始在64位系统上会默认开启压缩指针。
三、总结
1. 通过指针压缩,利用对齐填充特性,通过映射方式达到了内存地址扩展的效果
2. 指针压缩能够节省内存空间,同时提高了程序的寻址效率
3. 堆内存设置最好不要超过32GB,这时指针压缩将会失效,造成空间的浪费。
原因:
1)指针压缩依赖于堆内存的大小。为了实现指针压缩,JVM 会假定对象引用(指针)指向的内存地址空间最大为 32GB。因此,当堆内存设置超过 32GB 时,JVM 就无法再使用 4 字节来表示每个对象的地址了,因为地址空间已经超出了 32GB 的限制。
2)当堆内存超过 32GB 时,JVM 会自动禁用指针压缩,因为指针压缩的优化只有在地址范围较小的时候才有意义。如果堆内存更大,指针的地址空间超出了压缩的范围,那么就没有办法继续使用 4 字节的指针了
4. 指针压缩不仅可以作用于对象头的类型指针,还可以作用于引用类型的字段指针(包括引用类型的数组指针)
5. -XX:ObjectAlignmentInBytes,默认是 8,也就是 8 字节对齐
参考链接:
https://blog.csdn.net/liujianyangbj/article/details/108049482
https://www.cnblogs.com/star95/p/17512212.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
2022-11-06 研发流程