
本文将讨论Java虚拟机64位中的指针压缩的实现,该虚拟机由UseCompressedOops选项控制,并且默认情况下从Java SE 6u23开始对64位系统启用。
问题描述
在64位JVM中,指针占用的内存空间(惊喜)是32位内存的两倍。 与32位架构的相同代码相比,这可以使数据大小增加1.5倍。 同时,在32位体系结构中,只能寻址2 ^ 32字节(4 GB),这在现代世界中是很小的。
让我们编写一个小程序,看看Integer对象占用多少字节:
import java.util.stream.IntStream; import java.util.stream.Stream; class HeapTest { public static void main(String ... args) throws Exception { Integer[] x = IntStream.range(0, 1_000_000).boxed().toArray(Integer[]::new); Thread.sleep(6000000); Stream.of(x).forEach(System.out::println); } }
在这里,我们突出显示了Integer类的一百万个对象,并长时间入睡。 最后一行是必需的,以便编译器不会突然忽略数组的创建(尽管在我的机器上,对象的创建通常没有此行)。
我们在禁用指针压缩的情况下编译并运行程序:
> javac HeapTest.java > java -XX:-UseCompressedOops HeapTest
使用jcmd实用程序, 我们查看内存分配:
> jps 45236 HeapTest ... > jcmd 45236 GC.class_histogram

图片显示对象总数为1000128 ,这些对象占用的内存大小为24003072字节 。 即 每个对象24个字节(为什么下面要写24个字节)。
这是同一程序的内存,但UseCompressedOops标志处于打开状态 :

现在每个对象占用16个字节 。
压缩的优点很明显=)
解决方案
JVM如何压缩指针? 这项技术称为压缩哎呀 。 Oop代表普通对象指针或普通对象指针 。
诀窍是在64位系统中,内存中的数据与机器字对齐,即 每个8字节。 该地址的末尾总是三个零位。
如果通过将地址向右移动3位来保存指针(该操作称为编码 ),并且在使用之前将地址向左移动3位(分别为解码 ),则可以容纳35位大小的32位指针,即 地址最大为32 GB (2 ^ 35字节)。
如果程序的堆大小超过32GB,则压缩将停止工作,并且所有指针的大小将变为8个字节。
启用UseCompressedOops选项时,将压缩以下类型的指针:
JVM本身的对象从不压缩。 在这种情况下,压缩发生在虚拟机级别,而不是字节码。
阅读有关在内存中放置对象的更多信息
现在,让我们使用jol实用程序(Java对象布局)来仔细了解Integer在不同JVM中占用的内存量:
> java -jar jol-cli-0.9-full.jar estimates java.lang.Integer ***** 32-bit VM: ********************************************************** java.lang.Integer object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 8 (object header) N/A 8 4 int Integer.value N/A 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total ***** 64-bit VM: ********************************************************** java.lang.Integer object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 16 (object header) N/A 16 4 int Integer.value N/A 20 4 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total ***** 64-bit VM, compressed references enabled: *************************** java.lang.Integer object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int Integer.value N/A Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total ***** 64-bit VM, compressed references enabled, 16-byte align: ************ java.lang.Integer object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int Integer.value N/A Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
“ 64位VM”和“ 64位VM,启用了压缩引用”之间的区别是将对象标头减少4个字节。 另外,在没有压缩的情况下,有必要再增加4个字节来对齐内存中的数据 。
这个对象头是什么? 为什么减少4个字节?

该图像显示了12个字节的对象标头,即 启用UseCompressedOops选项。 标头由一些内部JVM标志以及指向此对象的类的指针组成。 可以看出,指向该类的指针需要32位。 如果不进行压缩,它将占用64位,并且对象标头的大小已经是16个字节。
顺便说一下,您可以看到还有另一种16字节对齐的选项。 在这种情况下,您可以将内存增加到最大64 GB。
指针的缺点压缩
当然,压缩指针具有明显的缺点-每次访问指针时进行编码和解码操作的成本。 确切的数字将因应用程序而异。
例如,这是一张压缩的和未压缩的指针的暂停垃圾回收器暂停的图表,摘自此处的Java GC,编号为-压缩的OOP。

可以看出,打开压缩后,GC暂停的时间更长。 您可以在文章本身中阅读有关此内容的更多信息(该文章很旧-2013)。
参考文献:
热点JVM中的压缩oop
JVM如何分配对象
CompressedOops:Java压缩参考简介
欺骗JVM的压缩Oop
Java HotSpot虚拟机性能增强