1. 概述

在 C/C++ 中,我们可以使用 sizeof() 运算符直接获取对象占用的字节数。但在 Java 中,并没有类似的原生操作符。✅

这并不意味着我们完全无法估算对象大小。本文将带你了解 Java 对象内存占用的底层机制,并介绍一种相对准确的估算方式——使用 Instrumentation 接口。⚠️ 注意:这不是精确值,而是 JVM 提供的“实现相关近似值”。


2. Java 中的内存消耗机制

虽然 Java 没有 sizeof,但理解内存布局仍然至关重要,尤其在做性能优化或踩过 OOM 坑之后。❌

JVM 对内存的管理是高度抽象的。尽管基本类型有标准大小(如 int 为 4 字节),但 JVM 内部可以自由决定如何存储数据——包括使用填充字节、堆外存储、栈上分配甚至 JIT 优化掉某些变量。只要程序行为不变,这些底层实现都是合法的。

因此,我们只能粗略估算实际 RAM 占用,尤其是在考虑 CPU 缓存层级(L1/L2/L3)可能导致数据副本的情况下。

2.1 对象、引用与包装类的内存开销

以下是现代 64 位 JDK(启用指针压缩)下的典型内存布局:

  • 对象头(Object Header)

    • 12 字节(64 位 JVM,默认开启 -XX:+UseCompressedOops
    • 对象总大小会对齐到 8 字节的倍数,所以最小对象大小为 16 字节
  • 引用(Reference)大小

    • 堆内存 ≤ 32GB 时:4 字节(指针压缩生效)
    • 堆内存 > 32GB 时:8 字节
    • 因此,64 位 JVM 通常比 32 位多消耗 30%-50% 的堆空间
  • 包装类与容器的代价

    • int 原始类型:4 字节
    • Integer 对象:16 字节(含对象头 + value 字段)
    • 内存开销高达 300%
    • 同理,String、数组、集合类(如 ArrayList)都有额外元数据开销

💡 小结:频繁使用包装类或小对象集合时,务必警惕内存膨胀问题。


3. 使用 Instrumentation 估算对象大小

最靠谱的估算方式是使用 JDK 提供的 java.lang.instrument.Instrumentation 接口,其 getObjectSize(Object) 方法可返回对象的浅层大小(shallow size)。

⚠️ 注意:

  • 返回的是 JVM 实现相关的近似值
  • 仅包含对象自身大小,不包含它引用的对象(即非递归)
  • 多次运行结果可能略有差异

3.1 创建 Instrumentation Agent

要使用 Instrumentation,必须通过 Java Agent 机制加载。我们需要实现 premain 方法,并暴露一个静态工具方法来访问 Instrumentation 实例。

public class InstrumentationAgent {
    private static volatile Instrumentation globalInstrumentation;

    public static void premain(final String agentArgs, final Instrumentation inst) {
        globalInstrumentation = inst;
    }

    public static long getObjectSize(final Object object) {
        if (globalInstrumentation == null) {
            throw new IllegalStateException("Agent not initialized.");
        }
        return globalInstrumentation.getObjectSize(object);
    }
}

打包 Agent JAR

需要创建一个包含 MANIFEST.MF 的 JAR 文件,声明 Premain-Class

Premain-Class: com.example.InstrumentationAgent

通过命令行打包:

javac InstrumentationAgent.java
jar cmf MANIFEST.MF InstrumentationAgent.jar InstrumentationAgent.class

3.2 示例代码与输出

下面是一个测试类,用于打印各类对象的估算大小:

public class InstrumentationExample {

    public static void printObjectSize(Object object) {
        System.out.println("Object type: " + object.getClass() +
          ", size: " + InstrumentationAgent.getObjectSize(object) + " bytes");
    }

    public static void main(String[] arguments) {
        String emptyString = "";
        String string = "Estimating Object Size Using Instrumentation";
        String[] stringArray = { emptyString, string, "com.example" };
        String[] anotherStringArray = new String[100];
        List<String> stringList = new ArrayList<>();
        StringBuilder stringBuilder = new StringBuilder(100);
        int maxIntPrimitive = Integer.MAX_VALUE;
        int minIntPrimitive = Integer.MIN_VALUE;
        Integer maxInteger = Integer.MAX_VALUE;
        Integer minInteger = Integer.MIN_VALUE;
        long zeroLong = 0L;
        double zeroDouble = 0.0;
        boolean falseBoolean = false;
        Object object = new Object();

        class EmptyClass {}
        EmptyClass emptyClass = new EmptyClass();

        class StringClass {
            public String s;
        }
        StringClass stringClass = new StringClass();

        printObjectSize(emptyString);
        printObjectSize(string);
        printObjectSize(stringArray);
        printObjectSize(anotherStringArray);
        printObjectSize(stringList);
        printObjectSize(stringBuilder);
        printObjectSize(maxInteger);
        printObjectSize(minInteger);
        printObjectSize(zeroLong);
        printObjectSize(zeroDouble);
        printObjectSize(falseBoolean);
        printObjectSize(Day.TUESDAY);
        printObjectSize(object);
        printObjectSize(emptyClass);
        printObjectSize(stringClass);
    }

    public enum Day {
        MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
    }
}

启动参数

运行时必须通过 -javaagent 参数加载 Agent:

java -javaagent:InstrumentationAgent.jar com.example.InstrumentationExample

输出结果(示例)

Object type: class java.lang.String, size: 24 bytes
Object type: class java.lang.String, size: 24 bytes
Object type: class [Ljava.lang.String;, size: 32 bytes
Object type: class [Ljava.lang.String;, size: 416 bytes
Object type: class java.util.ArrayList, size: 24 bytes
Object type: class java.lang.StringBuilder, size: 24 bytes
Object type: class java.lang.Integer, size: 16 bytes
Object type: class java.lang.Integer, size: 16 bytes
Object type: class java.lang.Long, size: 24 bytes
Object type: class java.lang.Double, size: 24 bytes
Object type: class java.lang.Boolean, size: 16 bytes
Object type: class com.example.InstrumentationExample$Day, size: 24 bytes
Object type: class java.lang.Object, size: 16 bytes
Object type: class com.example.InstrumentationExample$1EmptyClass, size: 16 bytes
Object type: class com.example.InstrumentationExample$1StringClass, size: 16 bytes

结果分析

  • String: 24 字节(对象头 12 + char[] 引用 4 + hash 4 + 对齐 4)
  • Integer: 16 字节(对象头 12 + int value 4)
  • Long: 24 字节(对象头 12 + long value 8 + 对齐 4)
  • 空对象(如 Object, EmptyClass): 16 字节(最小对齐单位)

4. 总结

  • Java 没有 sizeof,但可通过 Instrumentation.getObjectSize() 获取对象的浅层大小估算值
  • 该方法依赖 Java Agent,需在启动时通过 -javaagent 加载
  • 返回值为 JVM 实现相关,仅作参考,不可用于跨环境精确计算
  • 包装类、小对象、集合类存在显著内存开销,高并发或大数据场景下应谨慎使用
  • 若需计算深拷贝大小(包含引用对象),需自行递归遍历对象图并累加

💡 实际项目中,推荐结合 JOL(Java Object Layout)工具进行更深入的内存分析,比手动 Agent 更强大直观。

完整代码示例可参考:GitHub - core-java-jvm


原始标题:How to Get the Size of an Object in Java