在Java中,对象的大小并不是固定的,它取决于几个因素,包括对象中的数据类型、对象头的大小、对齐填充(padding)以及继承的字段。下面是一些用于估算Java对象大小的基本准则:
-
对象头(Object Header): 每个Java对象都有一个对象头,它包含了一些用于管理对象的元数据,比如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。在32位JVM上,对象头通常是8字节,而在64位JVM上(没有使用压缩指针的情况下)是16字节。
-
实例数据(Instance Data): 对象中每个字段的大小取决于其类型。例如,
int
是4字节,long
是8字节,引用(在使用压缩指针的64位JVM上)通常是4字节等。 -
对齐填充(Alignment Padding): JVM通常要求对象的大小是8字节的倍数。如果对象的大小不是8的倍数,那么就会添加填充以达到这个要求。
为了计算一个对象的大小,你可以遵循以下步骤:
- 计算对象头的大小。
- 为对象中的每个字段加上相应的大小。
- 如果需要,加上对齐填充。
Java中的对象对齐填充(Alignment Padding)是由于Java虚拟机(JVM)在内存分配时要求对象的大小必须是一定的倍数(通常是8字节)。这是为了提高性能,因为现代硬件和操作系统通常以这样的边界对齐访问内存更高效。当对象的实际大小不是对齐基数(如8字节)的倍数时,JVM会添加额外的字节以确保对象的总大小是对齐基数的倍数。这些额外的字节就是对齐填充。
对齐填充的情况通常发生在以下几个时候:
-
对象头之后:在对象头和实例数据之间,如果对象头的大小加上特定的字段大小不能整除对齐基数,JVM将会在字段之前添加填充。
-
字段之间:对于对象中的字段,如果前一个字段的大小加上其地址偏移不能整除对齐基数,则在该字段和下一个字段之间可能会添加填充。
-
对象末尾:在对象的最后一个字段之后,如果所有字段的总大小加上对象头的大小不能整除对齐基数,JVM将会添加填充以确保整个对象的大小是对齐基数的倍数。
举个例子:
public class Example {
char c; // 2 bytes
int i; // 4 bytes
}
在一个64位JVM上,如果没有开启压缩指针,则对象头通常是16字节。那么对于上面的Example
类:
- 对象头: 16字节
char c
: 2字节- 填充: 2字节(因为
int
类型需要4字节对齐,所以在char
和int
之间添加2字节的填充) int i
: 4字节
总计:16 + 2 + 2 + 4 = 24字节。由于24字节已经是8的倍数,所以对象末尾不需要额外的填充。
请注意,这些都是理论上的计算,实际的对象布局可能会因JVM的具体实现和版本而有所不同。使用专业工具(如JOL)可以帮助你准确地了解特定JVM上对象的内存布局。
然而,这只是一个粗略的估算。实际对象大小可能因JVM实现的不同而有所变化。如果你需要精确计算对象的大小,可以使用一些工具和技术,比如:
- 使用
Instrumentation
接口。Java提供了一个Instrumentation
接口,可以用来在运行时查询对象的大小。为了使用它,你需要启用一个Java代理,该代理在JVM启动时加载。
import java.lang.instrument.Instrumentation;
public class ObjectSizeFetcher {
private static volatile Instrumentation instr;
public static void premain(String args, Instrumentation inst) {
instr = inst;
}
public static long getObjectSize(Object o) {
if (instr == null) {
throw new IllegalStateException("Instrumentation is null");
}
return instr.getObjectSize(o);
}
}
你需要在JVM启动时指定这个代理,使用如下参数:
-javaagent:path/to/agent.jar
-
使用第三方库。例如,Eclipse Memory Analyzer (MAT) 或 JOL (Java Object Layout) 可以帮助你分析对象在内存中的布局。
-
使用JVM工具。例如,
jmap
可以用来生成堆转储,然后可以用jhat
或 MAT 分析这个转储。
需要注意的是,对象的实际内存使用还会涉及到其他因素,例如JVM内部的数据结构、GC算法的细节等。