JVM CompressedOops 压缩指针

·  阅读 471

普通对象指针 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 的倍数

image.png

关于对象大小及内存结构,参考 JAVA 对象内存布局

JVM 就是利用上面的特性实现了 CompressedOops,具体算法如下

对象的直接内存地址 = 堆的基址 + (32bit 的 CompressedOops * 64bit)

  • 对象的直接内存地址 :直接可以访问到对象实例的内存地址
  • 堆的基址 : 堆内存空间的起始地址

前面两块没什么好说的,重点是后面的 32bit 的 CompressedOops * 64bit

还是用 32 bit 来存储值,但这个这个值的不再用来表示实际的内存地址,而是用来表示偏移量,一个偏移量大小为 64 bit

例如上图中的对象,在启用压缩指针后,CompressedOops 如下

image.png

简单理解就是

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 多点就会发生压缩指针失效

参考

CompressedOops

hotspot 源码

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改