普通对象指针 OOPs
普通对象指针(ordinary object pointer, OOP) 是 HotSpot 中用来指向对象实例所在内存地址的指针,大小通常与 JVM 的位数相同,在 32 位 JVM 中,OOP 的大小为 32bit,在 64 位 JVM 中,OOP 的大小为 64bit
压缩指针 CompressedOops
为什么需要压缩指针
在 64 位 JVM 中,OOP 的大小为 64bit,可表达的数字范围是 0 ~ 2^64,如果这个数字表示的是内存地址的话,可表达的范围 0 ~ 2^64 bit ,换算成 GB 的话为 0 ~ 2147483648GB
但使用 64bit 的 OOP 会带来一些问题
- OOP 存储开销变大: 在 32 位 JVM 中,OOP 的大小为 32bit,在 64 位 JVM 中,OOP 的大小为 64bit,直接翻了一倍
- CPU 缓存命中率降低:CPU 缓存的大小是有限的,OOP 变大后,可存储的数量就会变少,需要更加频繁换出缓存
- "性能"过剩:64bit 最大可以表示 2147483648GB 内存,现代操作系统目前根本不支持这么大的内存,而且程序也根本用不上这么大的内存地址
可见用 64 bit 存储 OOP 有些浪费,那如果在 64 位 JVM 中,使用 32 bit 存储 OOP,可以吗?32 bit 可表达的数字范围是 0 ~ 2^32 ,如果这个数字表示的是内存地址的话,可表达的范围 0 ~ 2^32 bit ,换算成 GB 的话为 0 ~ 4GB,而 4GB 的堆大小,对于一些程序来说可能又不够用,因此使用 32 bit 直接表达内存地址也不太合适!
从 Oracle JDK 1.6 update 14 开始,JVM 在 64 位系统上提供了一种新方案,可以在 32 bit 的空间大小下,将可表达内存地址空间从最大 4GB 提升到了 32GB,这样即节省了存储空间,又提升了可表达范围,这套方案叫做 CompressedOops(压缩指针)
压缩指针的实现原理
JVM 实现 CompressedOops 基于一个前提条件:在 64 位 JVM 中,对象实例的大小必须为 64bit 的倍数
关于对象大小及内存结构,参考 JAVA 对象内存布局
JVM 就是利用上面的特性实现了 CompressedOops,具体算法如下
对象的直接内存地址 = 堆的基址 + (32bit 的 CompressedOops * 64bit)
- 对象的直接内存地址 :直接可以访问到对象实例的内存地址
- 堆的基址 : 堆内存空间的起始地址
前面两块没什么好说的,重点是后面的 32bit 的 CompressedOops * 64bit
还是用 32 bit 来存储值,但这个这个值的不再用来表示实际的内存地址,而是用来表示偏移量,一个偏移量大小为 64 bit。
例如上图中的对象,在启用压缩指针后,CompressedOops 如下
简单理解就是
JVM 还是使用 32 bit 空间来存储值,只是这个值的单位变了,原来单位是 bit,直接对应内存地址,而开启压缩指针后,单位变成偏移量,需要换算一下才能得到实际的内存地址
利用这种 "单位放大" 的思想,其实可以将值放大任意倍,但放大之后,可表示的范围从原来连续的 0 ~ 2^32 变成离散的
[0*n, 1*n , 2*n, ... 2^32*n]其中 n 是要放大的倍数。(所以说,计算机的底层是数学)
而 JVM 之所以选择 64 倍,是因为 “在 64 位 JVM 中,对象实例的大小必须为 64bit 的倍数” 这样一个前提,这样放大之后,即使内存地址从连续变成离散的,依然不会影响对象实例的存取
Zero Based Compressd Oops
零基压缩指针(Zero Based Compressd Oops),是对 CompressdOops 的进一步,它通过改变正常指针的随机分配地址的特性,强制堆的基址从零开始分配(需要 OS 支持),进一步提高了压缩解压效率。
压缩指针的适用范围
首先,只有 Oracle JDK 版本大于等于 1.6 update 14,并且 JVM 为 64 位才提供压缩指针功能,32 位 JVM 是不提供的。
其次,即使开启了压缩指针,JVM 也并非会压缩所有的指针,具体哪些指针会被压缩哪些指针不会被压缩,参考如下
**堆中会被压缩的 oops **
- 对象头中的 Klass Pointer
- 每个 oop 实例字段
- oop 数组的每个元素 (objArray)
不会被压缩的指针,在解释器中,oops 永远不会被压缩,包括
- 指向非堆的对象指针
- 栈中方法的局部变量
- 栈中方法的入参
- 栈中方法的返回值
- NULL 指针
压缩指针的使用
从Oracle JDK1.8 开始,64位的 JVM 默认开启压缩指针,如果想要手动调整,参考如下配置
# 开启指针压缩(默认)
-XX:+UseCompressedOops
# 关闭指针压缩
-XX:-UseCompressedOops
压缩指针失效
-
如果堆空间的大小在 4G 以下:不会使用压缩指针, JVM 会使用低虚拟地址空间(low virutal address space,64 位下模拟 32 位),这样可以避免压缩解压操作造成的 CPU 性能损耗
-
如果堆空间的大小在 4G 以上 32G 以下:启用 CompressedOop
-
如果堆空间的大小大于 32G :由于压缩指针最大只能表达 32G 堆内存空间,这会导致压缩指针无法使用 32G 以外的内存空间,因此当堆空间的大小大于 32G 时,不会启用 CompressedOop
从经验来看,40GB 的堆内存能存储的对象数量和 32GB 的堆内存能存储的对象数量大致相同
32GB 是个理论值,实际情况可能在 31GB 多点就会发生压缩指针失效