1. 概述
本文将深入探讨 Java 对象在 JVM 堆内存中实际占用的空间大小。
我们会先了解几种衡量对象大小的关键指标,然后通过多种方式实际测量对象实例的内存占用。
需要特别指出的是,JVM 运行时数据区的内存布局并未在 JVM 规范中强制定义,而是由具体实现自行决定(见 JVM 规范)。这意味着不同的 JVM 实现可能采用不同的对象和数组内存布局策略,进而影响运行时的对象大小。
本文聚焦于 HotSpot JVM 实现,后续提到的 JVM 均指 HotSpot JVM。
2. 浅层大小、保留大小与深层大小
分析对象内存占用时,我们通常使用三种度量方式:
✅ 浅层大小(Shallow Size)
仅计算对象自身占用的内存,不包括它引用的其他对象。如果对象包含引用字段,只计算引用指针本身的大小(4 或 8 字节),不包含被引用对象的实际内存。
图中 Triple
实例的浅层大小仅包含三个引用指针的大小,A1
、B1
、C1
三个对象本身的内存不计入。
✅ 深层大小(Deep Size)
在浅层大小的基础上,递归包含该对象所引用的所有对象的内存总和。
Triple
的深层大小 = 浅层大小 + A1
大小 + B1
大小 + C1
大小。
✅ 保留大小(Retained Size)
当垃圾回收器(GC)回收该对象时,能够释放的总内存量。即该对象被回收后,所有因它而存活、且不再被其他路径引用的对象内存总和。
Triple
的保留大小包含自身、A1
和 C1
,但不包含 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. 复杂对象内存计算
以 Professor
和 Course
类为例:
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
如图,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