Compressed OOPs in the JVM – JVM中的压缩OOPs

最后修改: 2019年 4月 12日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

The JVM manages memory for us. This removes the memory management burden from the developers, so we don’t need to manipulate object pointers manually, which is proven to be time consuming and error-prone.

JVM为我们管理内存。这消除了开发人员的内存管理负担,所以我们不需要手动操作对象指针,这被证明是耗时和容易出错的。

Under the hood, the JVM incorporates a lot of nifty tricks to optimize the memory management process. One trick is the use of Compressed Pointers, which we’re going evaluate in this article. First off, let’s see how the JVM represents objects at runtime.

在引擎盖下,JVM采用了许多巧妙的技巧来优化内存管理过程。其中一个技巧是使用压缩指针我们将在这篇文章中评估它。首先,让我们看看JVM在运行时如何表示对象。

2. Runtime Object Representation

2.运行时对象表示法

The HotSpot JVM uses a data structure called oops or Ordinary Object Pointers to represent objects. These oops are equivalent to native C pointers. The instanceOops are a special kind of oop that represents the object instances in Java. Moreover, the JVM also supports a handful of other oops that are kept in the OpenJDK source tree.

HotSpot JVM使用一种称为oopsOrdinary Object Pointers的数据结构来表示对象。这些oops相当于本地C语言的指针。instanceOops是一种特殊的oop,它在Java中代表对象实例。此外,JVM还支持其他一些oop,它们被保存在OpenJDK源代码树中。

Let’s see how the JVM lays out instanceOops in memory.

让我们看看JVM如何在内存中布置instanceOops。

2.1. Object Memory Layout

2.1.对象内存布局

The memory layout of an instanceOop is simple: it’s just the object header immediately followed by zero or more references to instance fields.

instanceOop的内存布局很简单:它只是对象的头,紧接着是零个或多个对实例字段的引用。

The JVM representation of an object header consists of:

对象头的JVM表示由以下部分组成。

  • One mark word serves many purposes such as Biased LockingIdentity Hash Values, and GC. It’s not an oop, but for historical reasons, it resides in the OpenJDK’s oop source tree. Also, the mark word state only contains a uintptr_ttherefore, its size varies between 4 and 8 bytes in 32-bit and 64-bit architectures, respectively
  • One, possibly compressed, Klass word, which represents a pointer to class metadata. Before Java 7, they were pointing to the Permanent Generation, but from the Java 8 onward, they are pointing to the Metaspace
  • A 32-bit gap to enforce object alignment. This makes the layout more hardware friendly, as we will see later

Immediately after the header, there are to be zero or more references to instance fields. In this case, a word is a native machine word, so 32-bit on legacy 32-bit machines and 64-bit on more modern systems.

在标题之后,将有零个或多个对实例字段的引用。在这种情况下,是一个本地机器字,所以在传统的32位机器上是32位,在更现代的系统上是64位。

The object header of arrays, in addition to mark and klass words, contains a 32-bit word to represent its length.

数组的对象头,除了mark和klass字之外,还包含一个32位字来表示其长度。

2.2. Anatomy of Waste

2.2.废物的解剖

Suppose we’re going to switch from a legacy 32-bit architecture to a more modern 64-bit machine. At first, we may expect to get an immediate performance boost. However, that’s not always the case when the JVM is involved.

假设我们要从一个传统的32位架构切换到一个更现代的64位机器。起初,我们可能期望立即获得性能提升。然而,当涉及到JVM时,情况并不总是如此。

The main culprit for this possible performance degradation is 64-bit object references. 64-bit references take up twice the space of 32-bit references, so this leads to more memory consumption in general and more frequent GC cycles. The more time dedicated to GC cycles, the fewer CPU execution slices for our application threads.

这种可能的性能下降的罪魁祸首是64位对象引用。64位引用占用的空间是32位引用的两倍,因此这导致了一般情况下更多的内存消耗和更频繁的GC循环。用于 GC 循环的时间越多,我们的应用程序线程的 CPU 执行片断就越少。

So, should we switch back and use those 32-bit architectures again? Even if this were an option, we couldn’t have more than 4 GB of heap space in 32-bit process spaces without a bit more work.

那么,我们是否应该换回来,重新使用那些32位架构呢?即使这是一个选择,我们也不可能在32位进程空间中拥有超过4GB的堆空间,而不需要再做一些工作。

3. Compressed OOPs

3.压缩的OOPs

As it turns out, the JVM can avoid wasting memory by compressing the object pointers or oops, so we can have the best of both worlds: allowing more than 4 GB of heap space with 32-bit references in 64-bit machines!

事实证明,JVM可以通过压缩对象指针或oops来避免浪费内存,因此我们可以获得两全其美的结果:在64位机器中允许超过4GB的堆空间与32位引用!

3.1. Basic Optimization

3.1.基本优化

As we saw earlier, the JVM adds padding to the objects so that their size is a multiple of 8 bytes. With these paddings, the last three bits in oops are always zero. This is because numbers that are a multiple of 8 always end in 000 in binary.

正如我们之前所看到的,JVM为对象添加了填充,使其大小为8字节的倍数。通过这些填充,oops中的最后三个比特总是为零。这是因为8的倍数的数字在二进制中总是以000结束。

Untitled-Diagram-2

Since the JVM already knows that the last three bits are always zero, there’s no point in storing those insignificant zeros in the heap. Instead, it assumes they are there and stores 3 other more significant bits that we couldn’t fit into 32-bits previously. Now, we have a 32-bit address with 3 right-shifted zeros, so we’re compressing a 35-bit pointer into a 32-bit one. This means that we can use up to 32 GB –  232+3=235=32 GB – of heap space without using 64-bit references.

由于JVM已经知道最后三个比特总是零,所以在堆中存储这些不重要的零是没有意义的。相反,它假定它们就在那里,并存储了另外3个更重要的位,而我们之前无法将其放入32位中。现在,我们有一个带有3个右移零的32位地址,所以我们把一个35位的指针压缩成一个32位的指针。这意味着我们最多可以使用32GB–232+3=235=32GB–的堆空间而无需使用64位引用。

In order to make this optimization work, when the JVM needs to find an object in memory it shifts the pointer to left by 3 bits (basically adds those 3-zeros back on to the end). On the other hand, when loading a pointer to the heap, the JVM shifts the pointer to right by 3 bits to discard those previously added zeros. Basically, the JVM performs a little bit more computation to save some space. Luckily, bit shifting is a really trivial operation for most CPUs.

为了使这种优化发挥作用,当JVM需要在内存中找到一个对象时,它将指针向左移动3位(基本上将这些3个零加到最后)。另一方面,当加载一个指针到堆时,JVM会将指针向右移动3位,以丢弃那些先前添加的零。基本上,JVM执行了多一点的计算以节省一些空间。幸运的是,对于大多数CPU来说,位移是一个非常微不足道的操作。

To enable oop compression, we can use the -XX:+UseCompressedOops tuning flag. The oop compression is the default behavior from Java 7 onwards whenever the maximum heap size is less than 32 GB. When the maximum heap size is more than 32 GB, the JVM will automatically switch off the oop compression. So memory utilization beyond a 32 Gb heap size needs to be managed differently.

要启用oop压缩,我们可以使用-XX:+UseCompressedOops调谐标志。op 压缩是Java 7以后的默认行为,只要最大堆大小小于32GB。当最大堆大小超过32GB时,JVM将自动关闭op 压缩。因此,超过32Gb堆大小的内存利用率需要不同的管理。

3.2. Beyond 32 GB

3.2.超过32GB

It’s also possible to use compressed pointers when Java heap sizes are greater than 32GB. Although the default object alignment is 8 bytes, this value is configurable using the -XX:ObjectAlignmentInBytes tuning flag. The specified value should be a power of two and must be within the range of 8 and 256.

当Java堆的大小大于32GB时,也可以使用压缩的指针。虽然默认的对象对齐方式是8字节,但是这个值可以使用-XX:ObjectAlignmentInBytes调整标志进行配置。指定的值应该是2的幂,并且必须在8和256的范围内

We can calculate the maximum possible heap size with compressed pointers as follows:

我们可以用压缩指针计算出最大可能的堆大小,如下所示。

4 GB * ObjectAlignmentInBytes

For example, when the object alignment is 16 bytes, we can use up to 64 GB of heap space with compressed pointers.

例如,当对象的对齐方式为16字节时,我们可以用压缩的指针使用多达64GB的堆空间。

Please note that as the alignment value increases, the unused space between objects might also increase. As a result, we may not realize any benefits from using compressed pointers with large Java heap sizes.

请注意,随着对齐值的增加,对象之间未使用的空间也可能增加。因此,我们可能无法从使用大的Java堆尺寸的压缩指针中实现任何好处。

3.3. Futuristic GCs

3.3.未来的GCs

ZGC, a new addition in Java 11, was an experimental and scalable low-latency garbage collector.

ZGCJava 11中新增加的内容,是一个实验性的、可扩展的低延迟垃圾收集器。

It can handle different ranges of heap sizes while keeping the GC pauses under 10 milliseconds. Since ZGC needs to use 64-bit colored pointersit does not support compressed references. So, using an ultra-low latency GC like ZGC has to be weighed against using more memory.

它可以处理不同范围的堆大小,同时将GC暂停时间保持在10毫秒以下。由于ZGC需要使用64位彩色指针它不支持压缩的引用。因此,使用像ZGC这样的超低延迟的GC,必须对使用更多的内存进行权衡。

As of Java 15, ZGC supports the compressed class pointers but still lacks the support for Compressed OOPs.

从Java 15开始,ZGC支持压缩类指针,但仍然缺乏对压缩OOPs的支持。

All new GC algorithms, however, won’t trade off memory for being low-latency. For instance, Shenandoah GC supports compressed references in addition to being a GC with low pause times.

然而,所有新的 GC 算法都不会以牺牲内存来换取低延迟。例如,Shenandoah GC除了是一个具有低暂停时间的GC之外,还支持压缩引用。

Moreover, both Shenandoah and ZGC are finalized as of Java 15.

此外,Shenandoah和ZGC都是最终确定的截止到Java 15

4. Conclusion

4.总结

In this article, we described a JVM memory management issue in 64-bit architectures. We looked at compressed pointers and object alignment, and we saw how the JVM can address these issues, allowing us to use larger heap sizes with less wasteful pointers and a minimum of extra computation.

在这篇文章中,我们描述了一个64位架构下的JVM内存管理问题。我们研究了压缩指针和对象对齐,我们看到了JVM如何解决这些问题,使我们能够以更少的浪费指针和最少的额外计算来使用更大的堆大小。

For a more detailed discussion on compressed references, it’s highly recommended to check out yet another great piece from Aleksey Shipilëv. Also, to see how object allocation works inside the HotSpot JVM, check out the Memory Layout of Objects in Java article.

关于压缩引用的更详细讨论,强烈建议查看Aleksey Shipilëv的另一篇精彩文章。此外,要了解对象分配在HotSpot JVM中的工作原理,请查看Java中对象的内存布局文章。