Java对象内存结构
Java 对象内存结构
对于具有 C/C++ 背景的开发者而言,Java 中缺乏直接计算对象占用内存大小的机制往往令人困惑。在 C/C++ 中,可以通过 sizeof 运算符获得基本类型及类实例的大小,这对指针运算、内存拷贝和 IO 操作非常有用。
Java 中并没有类似的运算符。事实上,Java 也不需要这种运算符。Java 基本类型的大小在语言规范中已明确定义(与 C/C++ 依赖平台不同),Java 拥有基于序列化的 IO 框架,且由于没有指针,指针运算和内存块拷贝等操作也不存在。
尽管如此,Java 程序员有时仍需了解一个对象到底占用了多少内存。这个问题的答案并不简单,首先需要区分 Shallow Size 和 Deep Size。
- Shallow Size(浅大小):指对象自身占用的内存大小,不包含其引用对象的大小。
- Deep Size(深大小):指对象自身所占内存大小,加上其递归引用的所有对象所占内存大小的总和。
大多数情况下,我们希望获得对象的 Deep Size,但为了计算该值,首先要掌握如何计算 Shallow Size。
注意:JVM 规范中并未针对运行时 Java 对象的内存结构做出强制说明,JVM 供应商可按需实现。这意味着同一个类在不同 JVM 上的实例对象占用内存大小可能存在差异。好在大多数用户(包括本文作者)使用的是 Sun HotSpot 虚拟机,这大大简化了问题。下文讨论均基于 32 位 Sun HotSpot JVM。
对象内存布局规则
以下规则用于辅助解释 JVM 如何组织对象在内存中的布局。
规则 1:对齐粒度
在 Sun JVM 中,除数组外,对象都有两个机器字(Word)的头部:
- 第一个字包含对象的标识哈希码(Identity Hash Code)以及锁状态等标识信息。
- 第二个字包含指向对象类的引用(Klass Pointer)。
此外,任何对象都以 8 个字节为粒度进行对齐。
规则 1:任何对象都是 8 个字节为粒度进行对齐的。
例如,调用 new Object() 时,由于 Object 类没有可存储的成员,仅使用堆中的 8 个字节保存两个字的头部即可。
规则 2:属性排列与对齐
除了 8 字节的头部,类属性紧随其后。属性通常根据其大小排列。例如,整型(int)以 4 字节为单位对齐,长整型(long)以 8 字节为单位对齐。这是出于性能考虑:数据若以 4 字节为单位对齐,从内存读取 4 字节数据写入处理器 4 字节寄存器的性价比更高。
为了节省内存,Sun JVM 并未按照属性声明顺序进行内存布局,而是按照以下优先级组织:
- 双精度型(
double)和长整型(long) - 整型(
int)和浮点型(float) - 短整型(
short)和字符型(char) - 布尔型(
boolean)和字节型(byte) - 引用类型(
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] 16Boolean 类的实例占用 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 版本及配置为准。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/java-dui-xiang-nei-cun-jie-gou.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。