Java 对象内存结构

对于具有 C/C++ 背景的开发者而言,Java 中缺乏直接计算对象占用内存大小的机制往往令人困惑。在 C/C++ 中,可以通过 sizeof 运算符获得基本类型及类实例的大小,这对指针运算、内存拷贝和 IO 操作非常有用。

Java 中并没有类似的运算符。事实上,Java 也不需要这种运算符。Java 基本类型的大小在语言规范中已明确定义(与 C/C++ 依赖平台不同),Java 拥有基于序列化的 IO 框架,且由于没有指针,指针运算和内存块拷贝等操作也不存在。

尽管如此,Java 程序员有时仍需了解一个对象到底占用了多少内存。这个问题的答案并不简单,首先需要区分 Shallow SizeDeep Size

  • Shallow Size(浅大小):指对象自身占用的内存大小,不包含其引用对象的大小。
  • Deep Size(深大小):指对象自身所占内存大小,加上其递归引用的所有对象所占内存大小的总和。

大多数情况下,我们希望获得对象的 Deep Size,但为了计算该值,首先要掌握如何计算 Shallow Size。

注意:JVM 规范中并未针对运行时 Java 对象的内存结构做出强制说明,JVM 供应商可按需实现。这意味着同一个类在不同 JVM 上的实例对象占用内存大小可能存在差异。好在大多数用户(包括本文作者)使用的是 Sun HotSpot 虚拟机,这大大简化了问题。下文讨论均基于 32 位 Sun HotSpot JVM。

对象内存布局规则

以下规则用于辅助解释 JVM 如何组织对象在内存中的布局。

规则 1:对齐粒度

在 Sun JVM 中,除数组外,对象都有两个机器字(Word)的头部:

  1. 第一个字包含对象的标识哈希码(Identity Hash Code)以及锁状态等标识信息。
  2. 第二个字包含指向对象类的引用(Klass Pointer)。

此外,任何对象都以 8 个字节为粒度进行对齐。

规则 1:任何对象都是 8 个字节为粒度进行对齐的。

例如,调用 new Object() 时,由于 Object 类没有可存储的成员,仅使用堆中的 8 个字节保存两个字的头部即可。

规则 2:属性排列与对齐

除了 8 字节的头部,类属性紧随其后。属性通常根据其大小排列。例如,整型(int)以 4 字节为单位对齐,长整型(long)以 8 字节为单位对齐。这是出于性能考虑:数据若以 4 字节为单位对齐,从内存读取 4 字节数据写入处理器 4 字节寄存器的性价比更高。

为了节省内存,Sun JVM 并未按照属性声明顺序进行内存布局,而是按照以下优先级组织:

  1. 双精度型(double)和长整型(long
  2. 整型(int)和浮点型(float
  3. 短整型(short)和字符型(char
  4. 布尔型(boolean)和字节型(byte
  5. 引用类型(reference

这种机制优化了内存使用率。例如,声明如下类:

class MyClass {
    byte a;
    int c;
    boolean d;
    long e;
    Object f;          
}

如果 JVM 不打乱属性声明顺序,对象内存布局如下:

[HEADER:  8 bytes]  8
[a:       1 byte ]  9
[padding: 3 bytes] 12
[c:       4 bytes] 16
[d:       1 byte ] 17
[padding: 7 bytes] 24
[e:       8 bytes] 32
[f:       4 bytes] 36
[padding: 4 bytes] 40

此时,用于占位(padding)的 14 个字节被浪费,对象共使用 40 字节。若按上述规则重新排序,内存布局变为:

[HEADER:  8 bytes]  8
[e:       8 bytes] 16
[c:       4 bytes] 20
[a:       1 byte ] 21
[d:       1 byte ] 22
[padding: 2 bytes] 24
[f:       4 bytes] 28
[padding: 4 bytes] 32

此次占位仅 6 字节,对象共使用 32 字节。

规则 2:类属性按照如下优先级进行排列:长整型和双精度类型;整型和浮点型;字符和短整型;字节类型和布尔类型,最后是引用类型。这些属性都按照各自的单位对齐。

现在我们可以计算继承自 Object 的类的实例内存大小。以 java.lang.Boolean 为例,其内存布局如下:

[HEADER:  8 bytes]  8 
[value:   1 byte ]  9
[padding: 7 bytes] 16

Boolean 类的实例占用 16 个字节的内存(别忘了最后用于占位的 7 个字节)。

规则 3:继承关系中的成员排列

JVM 遵守以下规则组织有父类的类的成员:

规则 3:不同类继承关系中的成员不能混合排列。首先按照规则 2 处理父类中的成员,接着才是子类的成员。

举例如下:

class A {
   long a;
   int b;
   int c;
}

class B extends A {
   long d;
}

B 的实例在内存中的存储如下:

[HEADER:  8 bytes]  8
[a:       8 bytes] 16
[b:       4 bytes] 20
[c:       4 bytes] 24
[d:       8 bytes] 32

规则 4:父类与子类间的填充

如果父类中成员的大小无法满足 4 个字节这个基本单位,下一条规则就会起作用:

规则 4:当父类中最后一个成员和子类第一个成员的间隔如果不够 4 个字节的话,就必须扩展到 4 个字节的基本单位。

举例如下:

class A {
   byte a;
}

class B extends A {
   byte b;
}

内存布局:

[HEADER:  8 bytes]  8
[a:       1 byte ]  9
[padding: 3 bytes] 12
[b:       1 byte ] 13
[padding: 3 bytes] 16

注意到成员 a 被扩充了 3 个字节以保证和成员 b 之间的间隔是 4 个字节。这个空间不能被类 B 使用,因此被浪费了。

规则 5:子类长整型优化

最后一条规则用于在特定情况下节省空间:如果子类成员是长整型或双精度类型,并且父类并没有用完 8 个字节。

规则 5:如果子类第一个成员是一个双精度或者长整型,并且父类并没有用完 8 个字节,JVM 会破坏规则 2,按照整型(int),短整型(short),字节型(byte),引用类型(reference)的顺序,向未填满的空间填充。

举例如下:

class A {
  byte a;
}

class B extends A {
  long b;
  short c;  
  byte d;
}

其内存布局如下:

[HEADER:  8 bytes]  8
[a:       1 byte ]  9
[padding: 3 bytes] 12
[c:       2 bytes] 14
[d:       1 byte ] 15
[padding: 1 byte ] 16
[b:       8 bytes] 24

在第 12 字节处,类 A“结束”的地方,JVM 没有遵守规则 2,而是在长整型之前插入一个短整型和一个字节型成员,这样可以避免浪费 3 个字节。

数组的内存布局

数组有一个额外的头部成员,用来存放“长度”变量。数组元素以及数组本身,跟其他常规对象同样,都需要遵守 8 个字节的边界规则。

下面是一个有 3 个元素的字节数组的内存布局:

[HEADER:  12 bytes] 12
[[0]:      1 byte ] 13
[[1]:      1 byte ] 14
[[2]:      1 byte ] 15
[padding:  1 byte ] 16

下面是一个有 3 个元素的长整型数组的内存布局:

[HEADER:  12 bytes] 12
[padding:  4 bytes] 16
[[0]:      8 bytes] 24
[[1]:      8 bytes] 32
[[2]:      8 bytes] 40

内部类的内存布局

非静态内部类(Non-static inner classes)有一个额外的“隐藏”成员,这个成员是一个指向外部类的引用变量。这个成员是一个普通引用,因此遵守引用内存布局的规则。内部类因此有 4 个字节的额外开销。

总结

我们已经学习了在 32 位 Sun JVM 中如何计算 Java 对象的 Shallow Size。知道内存是如何组织的有助于理解类实例占用的内存数。

后续文章将提供示例代码,利用反射(Reflection)来计算一个对象的 Deep Size。如果你感兴趣,请订阅此源或者等待这个博客的更新。

英文原文:Code Instructions


说明:本文内容基于 32 位 Sun HotSpot JVM 环境。现代主流 JVM 多为 64 位版本,且通常开启了指针压缩(Compressed Oops),对象头大小及对齐规则可能有所不同(例如 64 位 JVM 开启压缩时对象头通常为 12 字节,对齐仍为 8 字节)。实际内存占用请以具体 JVM 版本及配置为准。