1. Overview
1.概述
In this tutorial, we’re going to see how the JVM lays out objects and arrays in the heap.
在本教程中,我们将看到JVM如何在堆中布置对象和数组。
First, we’ll start with a little bit of theory. Then, we’ll explore the different object and array memory layouts in different circumstances.
首先,我们将从一点理论开始。然后,我们将探讨在不同情况下不同的对象和数组内存布局。
Usually, the memory layout of run-time data areas is not part of the JVM specification and is left to the discretion of the implementor. Therefore, each JVM implementation may have a different strategy to layout objects and arrays in memory. In this tutorial, we’re focusing on one specific JVM implementation: the HotSpot JVM.
通常,运行时数据区域的内存布局不是 JVM 规范的一部分,而是由实现者自行决定。因此,每个 JVM 实现都可能有不同的策略来布局内存中的对象和数组。在本教程中,我们将专注于一个特定的JVM实现:HotSpot JVM。
We also may use the JVM and HotSpot JVM terms interchangeably.
我们也可能交替使用JVM和HotSpot JVM术语。
2. Ordinary Object Pointers (OOPs)
2.普通对象指针(OOPs)
The HotSpot JVM uses a data structure called Ordinary Object Pointers (OOPS) to represent pointers to objects. All pointers (both objects and arrays) in the JVM are based on a special data structure called oopDesc. Each oopDesc describes the pointer with the following information:
HotSpot JVM使用一种称为普通对象指针(OOPS)的数据结构来表示对象的指针。JVM中的所有指针(包括对象和数组)都基于一种称为oopDesc的特殊数据结构。每个oopDesc用以下信息描述指针。
- One mark word
- One, possibly compressed, klass word
The mark word describes the object header. The HotSpot JVM uses this word to store identity hashcode, biased locking pattern, locking information, and GC metadata.
标记词描述了对象的头。HotSpot JVM使用这个词来存储身份哈希码、偏向锁定模式、锁定信息和GC元数据。
Moreover, the mark word state only contains a uintptr_t, therefore, its size varies between 4 and 8 bytes in 32-bit and 64-bit architectures, respectively. Also, the mark word for biased and normal objects are different. However, we’ll only consider normal objects as Java 15 is going to deprecate biased locking.
此外,标记字状态只包含一个uintptr_t,因此,在 32 位和 64 位架构中,其大小分别在 4 和 8 字节之间。而且,偏置对象和正常对象的标记字是不同的。然而,我们将只考虑正常对象,因为 Java 15 将会取消偏置锁。
Additionally, the klass word encapsulates the language-level class information such as class name, its modifiers, superclass info, and so on.
此外,klass词封装了语言级的类信息,如类名、其修饰语、超类信息等。
For normal objects in Java, represented as instanceOop, the object header consists of mark and klass words plus possible alignment paddings. After the object header, there may be zero or more references to instance fields. So, that’s at least 16 bytes in 64-bit architectures because of 8 bytes of the mark, 4 bytes of klass, and another 4 bytes for padding.
对于Java中的普通对象,表示为instanceOop,对象头由mark和klass字样加上可能的对齐填充物组成。在对象头之后,可能有零个或多个对实例字段的引用。因此,在64位架构中至少有16个字节,因为有8个字节的mark,4个字节的klass,以及另外4个字节的填充。
For arrays, represented as arrayOop, the object header contains a 4-byte array length in addition to mark, klass, and paddings. Again, that would be at least 16 bytes because of 8 bytes of the mark, 4 bytes of klass, and another 4 bytes for the array length.
对于以arrayOop表示的数组,除了mark、klass和paddings之外,对象头还包含一个4字节的数组长度。同样,由于8个字节的标记、4个字节的klass以及另外4个字节的数组长度,这将至少是16个字节。
Now that we know enough about theory, let’s see how memory layout works in practice.
现在我们对理论有了足够的了解,让我们看看内存布局在实践中是如何运作的。
3. Setting Up JOL
3.设置JOL
To inspect the memory layout of objects in the JVM, we’re going to use the Java Object Layout (JOL) quite extensively. Therefore, we need to add the jol-core dependency:
为了检查JVM中对象的内存布局,我们将相当广泛地使用Java对象布局(JOL)。因此,我们需要添加jol-core依赖项。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
4. Memory Layout Examples
4.存储器布局实例
Let’s start by looking at the general VM details:
让我们先看一下一般的虚拟机细节。
System.out.println(VM.current().details());
This will print:
这将打印。
# Running 64-bit HotSpot VM.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
This means that the references take 4 bytes, booleans and bytes take 1 byte, shorts and chars take 2 bytes, ints and floats take 4 bytes, and finally, longs and doubles take 8 bytes. Interestingly, they consume the same amount of memory if we use them as array elements.
这意味着引用需要4个字节,booleans和bytes需要1个字节,shorts和chars需要2个字节,ints和floats需要4个字节,最后,longs和doubles需要8个字节。有趣的是,如果我们把它们作为数组元素使用,它们消耗的内存量是一样的。
Also, if we disable compressed references via -XX:-UseCompressedOops, only the reference size changes to 8 bytes:
另外,如果我们通过-XX:-UseCompressedOops禁用压缩的引用,只有引用大小变为8字节。
# Field sizes by type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
4.1. Basic
4.1.基本
Let’s consider a SimpleInt class:
让我们考虑一个SimpleInt类。
public class SimpleInt {
private int state;
}
If we print its class layout:
如果我们打印它的类布局。
System.out.println(ClassLayout.parseClass(SimpleInt.class).toPrintable());
We would see something like:
我们会看到类似的情况。
SimpleInt object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 int SimpleInt.state N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
As shown above, the object header is 12 bytes, including 8 bytes of the mark and 4 bytes of klass. After that, we have 4 bytes for the int state. In total, any object from this class would consume 16 bytes.
如上所示,对象头是12个字节,包括8个字节的标记和4个字节的klass。之后,我们有4个字节用于int state。总的来说,这个类的任何对象都会消耗16个字节。
Also, there is no value for the object header and the state because we’re parsing a class layout, not an instance layout.
另外,没有对象头和状态的值,因为我们解析的是一个类的布局,而不是一个实例布局。
4.2. Identity Hash Code
4.2.身份哈希代码
The hashCode() is one of the common methods for all Java objects. When we don’t declare a hashCode() method for a class, Java will use the identity hash code for it.
hashCode()是所有Java对象的通用方法之一。当我们没有为一个类声明hashCode()方法时,Java将为其使用身份哈希代码。
The identity hash code won’t change for an object during its lifetime. Therefore, the HotSpot JVM stores this value in the mark word once it’s computed.
一个对象的身份哈希代码在其生命周期内不会改变。因此,HotSpot JVM一旦计算出这个值,就将其存储在标记词中。
Let’s see the memory layout for an object instance:
让我们看看一个对象实例的内存布局。
SimpleInt instance = new SimpleInt();
System.out.println(ClassLayout.parseInstance(instance).toPrintable());
The HotSpot JVM computes the identity hash code lazily:
HotSpot JVM懒洋洋地计算身份哈希代码。
SimpleInt object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) # mark
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) # mark
8 4 (object header) 9b 1b 01 f8 (10011011 00011011 00000001 11111000) (-134145125) # klass
12 4 int SimpleInt.state 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
As shown above, the mark word currently doesn’t seem to store anything significant yet.
如上所示,标记词目前似乎还没有存储任何重要的东西。
However, this will change if we call the System.identityHashCode() or even Object.hashCode() on the object instance:
然而,如果我们在对象实例上调用System.identityHashCode()甚至Object.hashCode(),这将发生变化。
System.out.println("The identity hash code is " + System.identityHashCode(instance));
System.out.println(ClassLayout.parseInstance(instance).toPrintable());
Now, we can spot the identity hash code as part of the mark word:
现在,我们可以发现身份哈希代码作为标记词的一部分。
The identity hash code is 1702146597
SimpleInt object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 25 b2 74 (00000001 00100101 10110010 01110100) (1957831937)
4 4 (object header) 65 00 00 00 (01100101 00000000 00000000 00000000) (101)
8 4 (object header) 9b 1b 01 f8 (10011011 00011011 00000001 11111000) (-134145125)
12 4 int SimpleInt.state 0
The HotSpot JVM stores the identity hashcode as “25 b2 74 65” in the mark word. The most significant byte is 65 since the JVM stores that value in little-endian format. Therefore, to recover the hash code value in decimal (1702146597), we have to read the “25 b2 74 65” byte sequence in reverse order:
HotSpot JVM将身份哈希码作为 “25 b2 74 65 “存储在标记字中。最有意义的字节是65,因为JVM以小-endian格式存储该值。因此,为了恢复十进制的哈希码值(1702146597),我们必须以相反的顺序读取 “25 b2 74 65 “字节序列。
65 74 b2 25 = 01100101 01110100 10110010 00100101 = 1702146597
4.3. Alignment
4.3.对齐
By default, the JVM adds enough padding to the object to make its size a multiple of 8.
默认情况下,JVM会给对象添加足够的填充,使其大小为8的倍数。
For instance, consider the SimpleLong class:
例如,考虑SimpleLong类。
public class SimpleLong {
private long state;
}
If we parse the class layout:
如果我们解析一下类的布局。
System.out.println(ClassLayout.parseClass(SimpleLong.class).toPrintable());
Then JOL will print the memory layout:
然后,JOL将打印内存布局。
SimpleLong object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 (alignment/padding gap)
16 8 long SimpleLong.state N/A
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
As shown above, the object header and the long state consume 20 bytes in total. To make this size a multiple of 8 bytes, the JVM adds 4 bytes of padding.
如上所示,对象头和长状态共消耗20字节。为了使这个大小成为8字节的倍数,JVM增加了4字节的填充。
We can also change the default alignment size via the -XX:ObjectAlignmentInBytes tuning flag. For instance, for the same class, the memory layout with -XX:ObjectAlignmentInBytes=16 would be:
我们也可以通过-XX:ObjectAlignmentInBytes调整标志来改变默认的对齐尺寸。例如,对于同一个类,-XX:ObjectAlignmentInBytes=16的内存布局将是。
SimpleLong object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 (alignment/padding gap)
16 8 long SimpleLong.state N/A
24 8 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 4 bytes internal + 8 bytes external = 12 bytes total
The object header and the long variable still consume 20 bytes in total. So, we should add 12 more bytes to make it a multiple of 16.
对象头和long变量仍然总共消耗20个字节。因此,我们应该再增加12个字节,使其成为16的倍数。
As shown above, it adds 4 internal padding bytes to start the long variable at offset 16 (enabling more aligned access). Then It adds the remaining 8 bytes after the long variable.
如上图所示,它添加了4个内部填充字节,在偏移量16处开始long 变量(实现更多对齐访问)。然后,它在long 变量之后添加剩余的8个字节。
4.4. Field Packing
4.4.现场包装
When a class has multiple fields, the JVM may distribute those fields in such a way as to minimize padding waste. For example, consider the FieldsArrangement class:
当一个类有多个字段时,JVM可能会以这样一种方式来分配这些字段,以尽量减少填充的浪费。例如,考虑FieldsArrangement类。
public class FieldsArrangement {
private boolean first;
private char second;
private double third;
private int fourth;
private boolean fifth;
}
The field declaration order and their order in memory layout are different:
字段的声明顺序和它们在内存布局中的顺序是不同的。
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 int FieldsArrangement.fourth N/A
16 8 double FieldsArrangement.third N/A
24 2 char FieldsArrangement.second N/A
26 1 boolean FieldsArrangement.first N/A
27 1 boolean FieldsArrangement.fifth N/A
28 4 (loss due to the next object alignment)
The main motivation behind this is to minimize padding waste.
这背后的主要动机是为了尽量减少填充物的浪费。
4.5. Locking
4.5.锁定
The JVM also maintains the lock information inside the mark word. Let’s see this in action:
JVM也维护标记字内的锁信息。让我们来看看这个动作。
public class Lock {}
If we create an instance of this class, the memory layout for it would be:
如果我们创建这个类的一个实例,,它的内存布局将是。
Lock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 85 23 02 f8
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
However, if we synchronize on this instance:
然而,如果我们在这个实例上进行同步。
synchronized (lock) {
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
The memory layout changes to:
内存布局变为。
Lock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) f0 78 12 03
4 4 (object header) 00 70 00 00
8 4 (object header) 85 23 02 f8
12 4 (loss due to the next object alignment)
As shown above, the bit-pattern for the mark word changes when we’re holding the monitor lock.
如上图所示,当我们持有显示器锁时,标记字的比特模式会发生变化。
4.6. Age and Tenuring
4.6.年龄和任期
To promote an object to the old generation (in generational GCs, of course), the JVM needs to keep track of the number of survivals for each object. As mentioned earlier, the JVM also maintains this information inside the mark word.
为了将一个对象晋升到老一代(当然是在代际GC中),JVM需要跟踪每个对象的存活数量。如前所述,JVM也在标记词内维护这一信息。
To simulate minor GCs, we’re going to create lots of garbage by assigning an object to a volatile variable. This way we can prevent possible dead code eliminations by the JIT compiler:
为了模拟次要的GC,我们将通过将一个对象分配给一个volatile变量来创造大量的垃圾。这样我们就可以防止JIT编译器可能出现的dead code elimination。
volatile Object consumer;
Object instance = new Object();
long lastAddr = VM.current().addressOf(instance);
ClassLayout layout = ClassLayout.parseInstance(instance);
for (int i = 0; i < 10_000; i++) {
long currentAddr = VM.current().addressOf(instance);
if (currentAddr != lastAddr) {
System.out.println(layout.toPrintable());
}
for (int j = 0; j < 10_000; j++) {
consumer = new Object();
}
lastAddr = currentAddr;
}
Every time a live object’s address changes, that’s probably because of minor GC and movement between survivor spaces. For each change, we also print the new object layout to see the aging object.
每次一个活体的地址发生变化,那可能是因为小的GC和生存空间之间的移动。对于每个变化,我们也会打印新的对象布局,以看到老化的对象。
Here’s how the first 4 bytes of the mark word changes over time:
下面是标记词的前4个字节如何随时间变化。
09 00 00 00 (00001001 00000000 00000000 00000000)
^^^^
11 00 00 00 (00010001 00000000 00000000 00000000)
^^^^
19 00 00 00 (00011001 00000000 00000000 00000000)
^^^^
21 00 00 00 (00100001 00000000 00000000 00000000)
^^^^
29 00 00 00 (00101001 00000000 00000000 00000000)
^^^^
31 00 00 00 (00110001 00000000 00000000 00000000)
^^^^
31 00 00 00 (00110001 00000000 00000000 00000000)
^^^^
4.7. False Sharing and @Contended
4.7 虚假的共享和@Contended
The jdk.internal.vm.annotation.Contended annotation (or sun.misc.Contended on Java 8) is a hint for the JVM to isolate the annotated fields to avoid false sharing.
jdk.internal.vm.annotation.Contended注解(或Java 8上的sun.misc.Contended)是对JVM的一个提示,以隔离注解的字段,避免虚假的共享。
Put simply, the Contended annotation adds some paddings around each annotated field to isolate each field on its own cache line. Consequently, this will impact the memory layout.
简单地说,Contended 注释在每个注释的字段周围添加了一些填充物,以将每个字段隔离在自己的缓存行中。因此,这将影响内存布局。
To better understand this, let’s consider an example:
为了更好地理解这一点,让我们考虑一个例子。
public class Isolated {
@Contended
private int v1;
@Contended
private long v2;
}
If we inspect the memory layout of this class, we’ll see something like:
如果我们检查这个类的内存布局,我们会看到类似的东西。
Isolated object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 128 (alignment/padding gap)
140 4 int Isolated.i N/A
144 128 (alignment/padding gap)
272 8 long Isolated.l N/A
Instance size: 280 bytes
Space losses: 256 bytes internal + 0 bytes external = 256 bytes total
As shown above, the JVM adds 128 bytes of padding around each annotated field. Cache line size in most modern machines is around 64/128 bytes, hence the 128 bytes padding. Of course, we can control the Contended padding size with -XX:ContendedPaddingWidth tuning flag.
如上所示,JVM会在每个注释字段周围添加128字节的填充。大多数现代机器的缓存行大小约为64/128字节,因此有128字节的填充。当然,我们可以用-XX:ContendedPaddingWidth 调整标记来控制Contended 填充的大小。
Please note that the Contended annotation is JDK internal, therefore we should avoid using it.
请注意,Contended annotation是JDK内部的,因此我们应该避免使用它。
Also, we should run our code with the -XX:-RestrictContended tuning flag; otherwise, the annotation wouldn’t take effect. Basically, by default, this annotation is meant for internal-only usage, and disabling the RestrictContended will unlock this feature for public APIs.
此外,我们应该使用-XX:-RestrictContended调整标志来运行我们的代码;否则,该注释将不会生效。基本上,在默认情况下,这个注解只适用于内部使用,禁用RestrictContended将为公共API解锁这个功能。
4.8. Arrays
4.8.数组
As we mentioned before, the array length is also part of the array oop. For instance, for a boolean array containing 3 elements:
正如我们之前提到的,数组长度也是数组OP的一部分。例如,对于一个包含3个元素的boolean数组。
boolean[] booleans = new boolean[3];
System.out.println(ClassLayout.parseInstance(booleans).toPrintable());
The memory layout looks like:
内存布局看起来像。
[Z object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # mark
4 4 (object header) 00 00 00 00 # mark
8 4 (object header) 05 00 00 f8 # klass
12 4 (object header) 03 00 00 00 # array length
16 3 boolean [Z.<elements> N/A
19 5 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 5 bytes external = 5 bytes total
Here, we have 16 bytes of object header containing 8 bytes of mark word, 4 bytes of klass word, and 4 bytes of length. Immediately after the object header, we have 3 bytes for a boolean array with 3 elements.
这里,我们有16个字节的对象头,包含8个字节的标记字,4个字节的类别字,和4个字节的长度。紧接着对象头之后,我们有3个字节的boolean 数组,有3个元素。
4.9. Compressed References
4.9.压缩引用
So far, our examples were executed in a 64-bit architecture with compressed references enabled.
到目前为止,我们的例子是在启用压缩引用的64位架构下执行的。
With 8 bytes alignment, we can use up to 32 GB of heap with compressed references. If we go beyond this limitation or even disable the compressed references manually, then the klass word would consume 8 bytes instead of 4.
通过 8 字节对齐,我们最多可以使用32 GB 的堆与压缩引用。如果我们超越这一限制,甚至手动禁用压缩引用,那么类字将消耗8个字节而不是4个字节。
Let’s see the memory layout for the same array example when the compressed oops are disabled with the -XX:-UseCompressedOops tuning flag:
让我们看看当用-XX:-UseCompressedOops调整标志禁用压缩OOPS时,同一阵列例子的内存布局。
[Z object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # mark
4 4 (object header) 00 00 00 00 # mark
8 4 (object header) 28 60 d2 11 # klass
12 4 (object header) 01 00 00 00 # klass
16 4 (object header) 03 00 00 00 # length
20 4 (alignment/padding gap)
24 3 boolean [Z.<elements> N/A
27 5 (loss due to the next object alignment)
As promised, now there are 4 more bytes for the klass word.
正如承诺的那样,现在还有4个字节的klass字。
5. Conclusion
5.总结
In this tutorial, we saw how the JVM lays out objects and arrays in the heap.
在本教程中,我们看到了JVM如何在堆中布置对象和数组。
For a more detailed exploration, it’s highly recommended to check out the oops section of the JVM source code. Also, Aleksey Shipilëv has a much more in-depth article in this area.
对于更详细的探索,强烈建议查看JVMoops部分的源代码。另外,Aleksey Shipilëv在这个领域有一篇更深入的文章。
Moreover, more examples of JOL are available as part of the project source code.
此外,更多的JOL的例子可作为项目源代码的一部分。
As usual, all the examples are available over on GitHub.
像往常一样,所有的例子都可以在GitHub上找到。