Measuring Object Sizes in the JVM – 测量JVM中的对象大小

最后修改: 2020年 7月 10日

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

1. Overview

1.概述

In this tutorial, we’re going to see how much space each object consumes in the Java heap.

在本教程中,我们将看到每个对象在Java堆中消耗多少空间。

First, we’ll get familiar with different metrics to calculate object sizes. Then, we’re going to see a few ways to measure instance sizes.

首先,我们将熟悉计算对象尺寸的不同指标。然后,我们将看到测量实例大小的几种方法。

Usually, the memory layout of runtime 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. This will, in turn, affect the instance sizes at runtime.

通常,运行时数据区域的内存布局不是 JVM 规范的一部分,而是由实现者自行决定。因此,每个JVM实现可能有不同的策略来布局内存中的对象和数组。这将反过来影响运行时的实例大小。

In this tutorial, we’re focusing on one specific JVM implementation: The HotSpot JVM.

在本教程中,我们将专注于一个特定的JVM实现。HotSpot JVM。

We also use the JVM and HotSpot JVM terms interchangeably throughout the tutorial.

我们还在整个教程中交替使用JVM和HotSpot JVM术语。

2. Shallow, Retained, and Deep Object Sizes

2.浅层、保留层和深层物体的尺寸

To analyze the object sizes, we can use three different metrics: Shallow, retained, and deep sizes.

为了分析对象的大小,我们可以使用三种不同的指标。浅层、保留和深层尺寸。

When computing the shallow size of an object, we only consider the object itself. That is, if the object has references to other objects, we only consider the reference size to the target objects, not their actual object size. For instance:

当计算一个对象的浅层尺寸时,我们只考虑该对象本身。也就是说,如果该对象对其他对象有引用,我们只考虑对目标对象的引用尺寸,而不是其实际的对象尺寸。比如说。

Shallow Size

As shown above, the shallow size of the Triple instance is only a sum of three references. We exclude the actual size of the referred objects, namely A1, B1, and C1, from this size.

如上所示,Triple 实例的浅层尺寸只是三个引用的总和。我们将被引用对象的实际大小,即A1,B1,C1,排除在这个大小之外。

On the contrary, the deep size of an object includes the size of all referred objects, in addition to the shallow size:

相反,一个对象的深层尺寸除了浅层尺寸外,还包括所有参考对象的尺寸:

Deep Size

Here the deep size of the Triple instance contains three references plus the actual size of A1, B1, and C1. Therefore, deep sizes are recursive in nature.

在这里,Triple 实例的深层尺寸包含三个引用,加上A1、B1、C1的实际尺寸。因此,深度大小在本质上是递归的。

When the GC reclaims the memory occupied by an object, it frees a specific amount of memory. That amount is the retained size of that object:

当GC回收一个对象所占用的内存时,它释放了一定量的内存。这个数额就是该对象的保留大小:

Retained Size

The retained size of the Triple instance only includes A1 and C1 in addition to the Triple instance itself. On the other hand, this retained size doesn’t include the B1, since the Pair instance also has a reference to B1. 

Triple实例的保留大小只包括A1C1,除了Triple实例本身。另一方面,这个保留的大小不包括B1,因为Pair实例也有对B1的引用。

Sometimes these extra references are indirectly made by the JVM itself. Therefore, calculating the retained size can be a complicated task.

有时这些额外的引用是由JVM本身间接进行的。因此,计算保留的大小可能是一项复杂的任务。

To better understand the retained size, we should think in terms of the garbage collection. Collecting the Triple instance makes the A1 and C1 unreachable, but the B1 is still reachable through another object. Depending on the situation, the retained size can be anywhere between the shallow and deep size.

为了更好地理解保留的大小,我们应该从垃圾收集的角度来考虑。收集Triple实例使得A1C1无法到达,但是B1仍然可以通过其他对象到达。根据不同的情况,保留的大小可以是浅层和深层大小之间的任何地方。

3. Dependency

3.依赖性

To inspect the memory layout of objects or arrays in the JVM, we’re going to use the Java Object Layout (JOL) tool. Therefore, we’ll 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. Simple Data Types

4.简单的数据类型

To have a better understanding of the size of more complex objects, we should first know how much space each simple data type consumes. To do that, we can ask the Java Memory Layout or JOL to print the VM information:

为了更好地了解更复杂的对象的大小,我们首先应该知道每个简单的数据类型消耗多少空间。为了做到这一点,我们可以要求Java Memory Layout或JOL打印VM信息。

System.out.println(VM.current().details());

The above code will print the simple data type sizes as following:

上述代码将打印简单的数据类型大小,如下所示。

# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# 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]

So here are the space requirements for each simple data type in the JVM:

因此,这里是JVM中每个简单数据类型的空间要求。

  • Object references consume 4 bytes
  • boolean and byte values consume 1 byte
  • short and char values consume 2 bytes
  • int and float values consume 4 bytes
  • long and double values consume 8 bytes

This is true in 32-bit architectures and also 64-bit architectures with compressed references in effect.

这在32位架构中是真实的,在压缩引用生效的64位架构中也是如此。

It’s also worth mentioning that all data types consume the same amount of memory when used as array component types.

值得一提的是,所有的数据类型在作为数组组件类型使用时,消耗的内存都是一样的。

4.1. Uncompressed References

4.1.未压缩的引用

If we disable the compressed references via -XX:-UseCompressedOops tuning flag, then the size requirements will change:

如果我们通过-XX:-UseCompressedOopstuning标志禁用压缩引用,那么尺寸要求将会改变。

# Objects are 8 bytes aligned.
# 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]

Now object references will consume 8 bytes instead of 4 bytes. The remaining data types still consume the same amount of memory.

现在对象引用将消耗8个字节而不是4个字节。其余的数据类型仍然消耗相同数量的内存。

Moreover, the HotSpot JVM also can’t use the compressed references when the heap size is more than 32 GB (unless we change the object alignment).

此外,当堆的大小超过32GB时,HotSpot JVM也不能使用压缩的引用(除非我们改变对象对齐方式)。

The bottom line is if we disable the compressed references explicitly or the heap size is more than 32 GB, the object references will consume 8 bytes.

底线是,如果我们明确禁用压缩的引用,或者堆的大小超过32GB,对象引用将消耗8个字节。

Now that we know the memory consumption for basic data types, let’s calculate it for more complex objects.

现在我们知道了基本数据类型的内存消耗,让我们来计算一下更复杂的对象。

5. Complex Objects

5.复杂物体

To calculate the size for complex objects, let’s consider a typical professor to course relationship:

为了计算复杂对象的大小,让我们考虑一个典型的教授与课程的关系。

public class Course {

    private String name;

    // constructor
}

Each Professor, in addition to the personal details, can have a list of Courses:

每位教授,除了个人资料外,还可以有一个课程的列表。

public class Professor {

    private String name;
    private boolean tenured;
    private List<Course> courses = new ArrayList<>();
    private int level;
    private LocalDate birthDay;
    private double lastEvaluation;

    // constructor
}

5.1. Shallow Size: the Course Class

5.1.浅层尺寸:课程

The shallow size of the Course class instances should include a 4-byte object reference (for name field) plus some object overhead. We can check this assumption using JOL:

课程类实例的浅层尺寸应该包括一个4字节的对象引用(用于名称字段),加上一些对象的开销。我们可以用JOL来检查这个假设。

System.out.println(ClassLayout.parseClass(Course.class).toPrintable());

This will print the following:

这将打印出以下内容。

Course object internals:
 OFFSET  SIZE               TYPE DESCRIPTION               VALUE
      0    12                    (object header)           N/A
     12     4   java.lang.String Course.name               N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

As shown above, the shallow size is 16 bytes, including a 4 bytes object reference to the name field plus the object header.

如上图所示,浅层大小为16字节,包括一个4字节的对象参考字段,加上对象头。

5.2. Shallow Size: the Professor Class

5.2.浅显的尺寸:教授

If we run the same code for the Professor class:

如果我们为教授类运行同样的代码。

System.out.println(ClassLayout.parseClass(Professor.class).toPrintable());

Then JOL will print the memory consumption for the Professor class like the following:

然后JOL将打印Professor 类的内存消耗,如下所示。

Professor object internals:
 OFFSET  SIZE                  TYPE DESCRIPTION                     VALUE
      0    12                       (object header)                 N/A
     12     4                   int Professor.level                 N/A
     16     8                double Professor.lastEvaluation        N/A
     24     1               boolean Professor.tenured               N/A
     25     3                       (alignment/padding gap)                  
     28     4      java.lang.String Professor.name                  N/A
     32     4        java.util.List Professor.courses               N/A
     36     4   java.time.LocalDate Professor.birthDay              N/A
Instance size: 40 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

As we probably expected, the encapsulated fields are consuming 25 bytes:

正如我们可能预期的那样,封装的字段要消耗25个字节。

  • Three object references, each of which consumes 4 bytes. So 12 bytes in total for referring to other objects
  • One int which consumes 4 bytes
  • One boolean which consumes 1 byte
  • One double which consumes 8 bytes

Adding the 12 bytes overhead of the object header plus 3 bytes of alignment padding, the shallow size is 40 bytes.

加上对象头的12字节开销和3字节的对齐填充,浅层大小为40字节。

The key takeaway here is, in addition to the encapsulated state of each object, we should consider the object header and alignment paddings when calculating different object sizes.

这里的关键启示是,除了每个对象的封装状态外,在计算不同对象的尺寸时,我们应该考虑对象的标题和对齐的镶边

5.3. Shallow Size: an Instance

5.3.浅显的尺寸:一个实例

The sizeOf() method in JOL provides a much simpler way to compute the shallow size of an object instance. If we run the following snippet:

JOL中的sizeOf()方法提供了一种更简单的方法来计算一个对象实例的浅层尺寸。如果我们运行下面的片段。

String ds = "Data Structures";
Course course = new Course(ds);

System.out.println("The shallow size is: " + VM.current().sizeOf(course));

It’ll print the shallow size as follows:

它将打印出如下的浅色尺寸。

The shallow size is: 16

5.4. Uncompressed Size

5.4.未压缩的尺寸

If we disable the compressed references or use more than 32 GB of the heap, the shallow size will increase:

如果我们禁用压缩的引用或使用超过32GB的堆,那么浅层的大小将增加。

Professor object internals:
 OFFSET  SIZE                  TYPE DESCRIPTION                               VALUE
      0    16                       (object header)                           N/A
     16     8                double Professor.lastEvaluation                  N/A
     24     4                   int Professor.level                           N/A
     28     1               boolean Professor.tenured                         N/A
     29     3                       (alignment/padding gap)                  
     32     8      java.lang.String Professor.name                            N/A
     40     8        java.util.List Professor.courses                         N/A
     48     8   java.time.LocalDate Professor.birthDay                        N/A
Instance size: 56 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

When the compressed references are disabled, the object header and object references will consume more memory. Therefore, as shown above, now the same Professor class consumes 16 more bytes.

当压缩引用被禁用时,对象头和对象引用将消耗更多的内存。因此,如上图所示,现在同一个教授类要多消耗16字节。

5.5. Deep Size

5.5.深度尺寸

To calculate the deep size, we should include the full size of the object itself and all of its collaborators. For instance, for this simple scenario:

为了计算深度尺寸,我们应该包括对象本身和所有合作者的全部尺寸。例如,对于这个简单的场景。

String ds = "Data Structures";
Course course = new Course(ds);

The deep size of the Course instance is equal to the shallow size of the Course instance itself plus the deep size of that particular String instance.

课程实例的深层大小等于课程实例本身的浅层大小加上该特定字符串实例的深层大小。

With that being said, let’s see how much space that String instance consumes:

既然如此,让我们看看这个String实例消耗了多少空间。

System.out.println(ClassLayout.parseInstance(ds).toPrintable());

Each String instance encapsulates a char[] (more on this later) and an int hashcode:

每个String 实例都封装了一个char[] (后面会详细介绍)和一个int hashcode。

java.lang.String 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)                           da 02 00 f8
     12     4   char[] String.value                              [D, a, t, a,  , S, t, r, u, c, t, u, r, e, s]
     16     4      int String.hash                               0
     20     4          (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

The shallow size of this String instance is 24 bytes, which include the 4 bytes of cached hash code, 4 bytes of char[] reference, and other typical object overhead.

这个String 实例的浅层尺寸是24字节,其中包括4字节的缓存哈希代码、4字节的char[] 引用,以及其他典型的对象开销。

To see the actual size of the char[], we can parse its class layout, too:

为了查看char[]的实际大小,我们也可以解析它的类布局。

System.out.println(ClassLayout.parseInstance(ds.toCharArray()).toPrintable());

The layout of the char[] looks like this:

char[]的布局看起来像这样。

[C 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)                           41 00 00 f8 
     12     4        (object header)                           0f 00 00 00
     16    30   char [C.<elements>                             N/A
     46     2        (loss due to the next object alignment)
Instance size: 48 bytes
Space losses: 0 bytes internal + 2 bytes external = 2 bytes total

So, we have 16 bytes for the Course instance, 24 bytes for the String instance, and finally 48 bytes for the char[]. In total, the deep size of that Course instance is 88 bytes.

因此,我们为课程实例设置了16个字节,为字符串实例设置了24个字节,最后为char[]设置了48个字节。总共,该课程实例的深度大小为88字节。

With the introduction of compact strings in Java 9, the String class is internally using a byte[] to store the characters:

随着compact strings在Java 9中的引入,String 类在内部使用byte[] 来存储字符。

java.lang.String object internals:
 OFFSET  SIZE     TYPE DESCRIPTION                               
      0     4          (object header)                         
      4     4          (object header)                           
      8     4          (object header)                           
     12     4   byte[] String.value # the byte array                             
     16     4      int String.hash                               
     20     1     byte String.coder # encodig                             
     21     3          (loss due to the next object alignment)

Therefore, on Java 9+, the total footprint of the Course instance will be 72 bytes instead of 88 bytes.

因此,在Java 9+上,Course instance的总足迹将是72字节,而不是88字节。

5.6. Object Graph Layout

5.6 对象图的布局

Instead of parsing the class layout of each object in an object graph separately, we can use the GraphLayout. With GraphLayot, we just pass the starting point of the object graph, and it’ll report the layout of all reachable objects from that starting point. This way, we can calculate the deep size of the starting point of the graph.

我们可以使用GraphLayout,而不是单独解析对象图中每个对象的类布局。使用GraphLayot,我们只需传递对象图的起点,它将报告从该起点出发的所有可达对象的布局。这样,我们就可以计算出图的起点的深度大小。

For instance, we can see the total footprint of the Course instance as follows:

例如,我们可以看到Course instance的总足迹如下。

System.out.println(GraphLayout.parseInstance(course).toFootprint());

Which prints the following summary:

其中打印出以下摘要。

Course@67b6d4aed footprint:
     COUNT       AVG       SUM   DESCRIPTION
         1        48        48   [C
         1        16        16   com.baeldung.objectsize.Course
         1        24        24   java.lang.String
         3                  88   (total)

That’s 88 bytes in total. The totalSize() method returns the total footprint of the object, which is 88 bytes:

这总共是88个字节。totalSize() 方法返回对象的总占用空间,也就是88字节。

System.out.println(GraphLayout.parseInstance(course).totalSize());

6. Instrumentation

6.仪器仪表

To calculate the shallow size of an object, we can also use the Java instrumentation package and Java agents. First, we should create a class with a premain() method:

为了计算一个对象的浅层尺寸,我们还可以使用Java instrumentation包和Java代理。首先,我们应该创建一个带有premain()方法的类。

public class ObjectSizeCalculator {

    private static Instrumentation instrumentation;

    public static void premain(String args, Instrumentation inst) {
        instrumentation = inst;
    }

    public static long sizeOf(Object o) {
        return instrumentation.getObjectSize(o);
    }
}

As shown above, we’ll use the getObjectSize() method to find the shallow size of an object. We also need a manifest file:

如上所示,我们将使用getObjectSize()方法来查找一个对象的浅层尺寸。我们还需要一个清单文件。

Premain-Class: com.baeldung.objectsize.ObjectSizeCalculator

Then using this MANIFEST.MF file, we can create a JAR file and use it as a Java agent:

然后使用这个MANIFEST.MF文件,我们可以创建一个JAR文件并将其作为一个Java代理。

$ jar cmf MANIFEST.MF agent.jar *.class

Finally, if we run any code with the -javaagent:/path/to/agent.jar argument, then we can use the sizeOf() method:

最后,如果我们运行任何带有-javaagent:/path/to/agent.jar参数的代码,那么我们可以使用sizeOf() 方法。

String ds = "Data Structures";
Course course = new Course(ds);

System.out.println(ObjectSizeCalculator.sizeOf(course));

This will print 16 as the shallow size of the Course instance.

这将打印出16作为课程实例的浅层尺寸。

7. Class Stats

7.班级统计资料

To see the shallow size of objects in an already running application, we can take a look at the class stats using the jcmd:

要查看已经运行的应用程序中对象的浅层尺寸,我们可以使用jcmd:看一下类的统计资料。

$ jcmd <pid> GC.class_stats [output_columns]

For instance, we can see each instance size and number of all the Course instances:

例如,我们可以看到每个实例的大小和所有课程实例的数量。

$ jcmd 63984 GC.class_stats InstSize,InstCount,InstBytes | grep Course 
63984:
InstSize InstCount InstBytes ClassName
 16         1        16      com.baeldung.objectsize.Course

Again, this is reporting the shallow size of each Course instance as 16 bytes.

同样,这是在报告每个课程实例的浅层尺寸为16字节。

To see the class stats, we should launch the application with the -XX:+UnlockDiagnosticVMOptions tuning flag.

为了看到类的统计资料,我们应该用-XX:+UnlockDiagnosticVMOptions调整标志启动应用程序。

8. Heap Dump

8.堆积物倾倒

Using heap dumps is another option to inspect the instance sizes in running applications. This way, we can see the retained size for each instance. To take a heap dump, we can use the jcmd as the following:

使用堆转储是检查运行中的应用程序中的实例大小的另一个选择。通过这种方式,我们可以看到每个实例的保留大小。要进行堆转储,我们可以使用jcmd,如下。

$ jcmd <pid> GC.heap_dump [options] /path/to/dump/file

For instance:

比如说。

$ jcmd 63984 GC.heap_dump -all ~/dump.hpro

This will create a heap dump in the specified location. Also, with the -all option, all reachable and unreachable objects will be present in the heap dump. Without this option, the JVM will perform a full GC before creating the heap dump.

这将在指定的位置创建一个堆转储。另外,使用-all 选项,所有可到达和不可到达的对象都将出现在堆转储中。如果没有这个选项,JVM将在创建堆转储之前执行一次完整的GC。

After getting the heap dump, we can import it into tools like Visual VM:

得到堆转储后,我们可以将其导入Visual VM等工具中。

As shown above, the retained size of the only Course instance is 24 bytes. As mentioned earlier, the retained size can be anywhere between shallow (16 bytes) and deep sizes (88 bytes).

如上所示,唯一的Course实例的保留大小是24字节。如前所述,保留的大小可以是浅层(16字节)和深层大小(88字节)之间的任何地方。

It’s also worth mentioning that the Visual VM was part of the Oracle and Open JDK distributions before Java 9. However, this is no longer the case as of Java 9, and we should download the Visual VM from its website separately.

值得一提的是,在Java 9之前,Visual VM是Oracle和Open JDK发行版的一部分。然而,从Java 9开始就不再是这样了,我们应该从其网站上单独下载Visual VM。

9. Conclusion

9.结语

In this tutorial, we got familiar with different metrics to measure object sizes in the JVM runtime. After that, we actually did measure instance sizes with various tools such as JOL, Java Agents, and the jcmd command-line utility.

在本教程中,我们熟悉了在JVM运行时测量对象大小的不同指标。之后,我们确实用各种工具测量了实例大小,如JOL、Java Agents和jcmd命令行工具。

As usual, all the examples are available over on GitHub.

像往常一样,所有的例子都可以在GitHub上找到