1. 概述

本文将深入探讨 Java 对象在 JVM 堆内存中实际占用的空间大小。

我们会先了解几种衡量对象大小的关键指标,然后通过多种方式实际测量对象实例的内存占用。

需要特别指出的是,JVM 运行时数据区的内存布局并未在 JVM 规范中强制定义,而是由具体实现自行决定(见 JVM 规范)。这意味着不同的 JVM 实现可能采用不同的对象和数组内存布局策略,进而影响运行时的对象大小。

本文聚焦于 HotSpot JVM 实现,后续提到的 JVM 均指 HotSpot JVM。

2. 浅层大小、保留大小与深层大小

分析对象内存占用时,我们通常使用三种度量方式:

✅ 浅层大小(Shallow Size)

仅计算对象自身占用的内存,不包括它引用的其他对象。如果对象包含引用字段,只计算引用指针本身的大小(4 或 8 字节),不包含被引用对象的实际内存。

例如: Shallow Size

图中 Triple 实例的浅层大小仅包含三个引用指针的大小,A1B1C1 三个对象本身的内存不计入。

✅ 深层大小(Deep Size)

在浅层大小的基础上,递归包含该对象所引用的所有对象的内存总和。

例如: Deep Size

Triple 的深层大小 = 浅层大小 + A1 大小 + B1 大小 + C1 大小。

✅ 保留大小(Retained Size)

当垃圾回收器(GC)回收该对象时,能够释放的总内存量。即该对象被回收后,所有因它而存活、且不再被其他路径引用的对象内存总和。

例如: Retained Size

Triple 的保留大小包含自身、A1C1,但不包含 B1 —— 因为 B1 还被 Pair 实例引用,即使 Triple 被回收,B1 依然可达。

⚠️ 保留大小受整个对象图引用关系影响,计算复杂,通常介于浅层和深层大小之间。

3. 依赖引入

我们将使用 OpenJDK 提供的 Java Object Layout (JOL) 工具来精确分析 JVM 中对象的内存布局。

引入依赖:

<dependency> 
    <groupId>org.openjdk.jol</groupId> 
    <artifactId>jol-core</artifactId>    
    <version>0.10</version> 
</dependency>

4. 基本数据类型内存占用

要理解复杂对象的大小,先得清楚基本类型的内存开销。使用 JOL 查看当前 JVM 信息:

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

输出示例:

# 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]

各类型内存占用(启用压缩指针时):

  • boolean / byte:1 字节
  • short / char:2 字节
  • int / float:4 字节
  • long / double:8 字节
  • ✅ 对象引用:4 字节(64位JVM,压缩开启)

⚠️ 数组元素大小与字段大小一致。

4.1 未压缩指针的情况

若通过 -XX:-UseCompressedOops 关闭压缩指针,或堆内存 > 32GB,引用大小变为 8 字节:

# Field sizes by type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

此时对象头和引用均占用更多内存,整体对象变大。

5. 复杂对象内存计算

ProfessorCourse 类为例:

public class Course {
    private String name;
    // constructor
}
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 Course 类的浅层大小

使用 JOL 分析:

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

输出:

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

✅ 浅层大小 = 12 字节对象头 + 4 字节 String 引用 = 16 字节

5.2 Professor 类的浅层大小

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

输出:

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

✅ 计算过程:

  • 字段:int(4) + double(8) + boolean(1) + 3 个引用(12) = 25 字节
  • 对象头:12 字节
  • 填充:3 字节(保证 8 字节对齐)
  • 总计:40 字节

⚠️ 踩坑提醒:boolean 只占 1 字节,但 JVM 会因对齐填充浪费空间,别以为它真省内存。

5.3 实例的浅层大小(简单方法)

JOL 提供更便捷的 sizeOf() 方法:

String ds = "Data Structures";
Course course = new Course(ds);
System.out.println("The shallow size is: " + VM.current().sizeOf(course));

输出:

The shallow size is: 16

5.4 关闭压缩指针的影响

若关闭压缩指针,Professor 实例大小变为 56 字节(对象头 16 字节 + 引用 8 字节 × 3),比 40 字节多了 16 字节。

5.5 深层大小计算

深层大小 = 自身浅层大小 + 所有直接/间接引用对象的总大小。

Course 实例为例:

String ds = "Data Structures";
Course course = new Course(ds);
  • Course 浅层:16 字节

  • String 实例大小:

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

    输出:

    java.lang.String object internals:
     OFFSET  SIZE     TYPE DESCRIPTION                               VALUE
          0     4          (object header)                           ...
          4     4          (object header)                           ...
          8     4          (object header)                           ...
         12     4   char[] String.value                              ...
         16     4      int String.hash                               0
         20     4          (loss due to the next object alignment)
    Instance size: 24 bytes
    
  • char[] 大小:

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

    输出:

    [C object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           ...
          4     4        (object header)                           ...
          8     4        (object header)                           ...
         12     4        (object header)                           ...
         16    30   char [C.<elements>                             N/A
         46     2        (loss due to the next object alignment)
    Instance size: 48 bytes
    

✅ 深层大小 = 16(Course) + 24(String) + 48(char[]) = 88 字节

⚠️ Java 9+ 使用紧凑字符串(Compact String),String 内部用 byte[] 存储,总大小降至 72 字节

5.6 使用 GraphLayout 计算深层大小

JOL 的 GraphLayout 可自动分析整个对象图:

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

输出:

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)

也可直接获取总数:

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

6. 使用 Instrumentation 计算浅层大小

通过 Java Agent 和 Instrumentation API 也能获取对象大小:

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);
    }
}

MANIFEST.MF

Premain-Class: com.baeldung.objectsize.ObjectSizeCalculator

打包为 agent.jar:

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

运行时添加 JVM 参数:

-javaagent:/path/to/agent.jar

使用:

System.out.println(ObjectSizeCalculator.sizeOf(course)); // 输出 16

7. 使用 jcmd 查看类统计

运行中的应用可通过 jcmd 查看类实例大小(浅层):

$ jcmd <pid> GC.class_stats InstSize,InstCount,InstBytes | grep Course

输出示例:

InstSize InstCount InstBytes ClassName
    16         1        16   com.baeldung.objectsize.Course

⚠️ 需启动时添加 -XX:+UnlockDiagnosticVMOptions

8. 通过堆转储(Heap Dump)分析

堆转储可分析对象的保留大小

生成堆转储:

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

使用 VisualVM 分析: retained size

如图,Course 实例的保留大小为 24 字节(浅层 16 + String 8?实际值取决于对象图)。

⚠️ Java 9+ VisualVM 已从 JDK 中移除,需从 官网 单独下载。

9. 总结

本文系统介绍了 JVM 中对象大小的三种度量方式,并通过 JOL、Instrumentation、jcmd 和堆转储等工具进行了实战分析。

核心要点:

  • ✅ 浅层大小 = 对象头 + 字段 + 填充
  • ✅ 深层大小 = 浅层 + 所有引用对象递归大小
  • ✅ 保留大小 = GC 可回收的总内存,依赖对象图
  • ✅ 64位JVM默认启用压缩指针(<32GB堆),引用占4字节
  • ✅ 对齐填充可能造成内存浪费,设计类时注意字段顺序

所有示例代码已上传至 GitHub: https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-jvm-2


原始标题:Measuring Object Sizes in the JVM