大家都知道,jvm中对象实例存储在堆中,对象的引用存储在栈中,而对象的元数据(类型数据)存储在方法区。在我们进行内存优化的过程中经常需要了解每个对象占用的内存大小。接下来我将介绍对象占用内存大小的计算方式。
Java的对象模型
java是面向对象的语言,每个对象都属于某个类。在HotSpot虚拟机中对象采用的是oop-klass模型。其实原理很简单:就是在方法区中生成一个Class类保存类信息(Klass),包含静态常量、静态方法、字节码、即时编译代码等元数据,而在堆中实例化该类的实例对象(oop),实例对象中保存了指向Class类的指针,这样便构成了oop-klass模型。这样做有一个好处就是:在实现多态时只需要在Class类中保存虚方法表来减少频繁的方法搜索,而实例对象无需保存虚方法表。
每个对象都有一个 mark work 头部,以及一个引用(klass pointer)指向Class类的信息。
- 在未开启 UseCompressedOops 的 64 位 JVM 上,对象头有 16 字节大小,即 8 字节的 mark word 和 8 字节的引用。
- 在开启 UseCompressedOops 的 64 位机器上,引用成了 4 字节,一共 12 字节。
java对象在内存中模型如下:
Java对象内存占用
对象大小分为:
- 自身的大小(Shadow heap size)
- 所引用对象的大小(Retained heap size)
class MyClass {int a;Object object;
}
如上图例子所示:myClass 实例创建出来之后,在内存中所占的大小就是 myClass 自身大小(Shadow heap size)。包括类的头部大小以及一个int的大小和一个引用的大小。myClass 中object 成员变量是一个对象引用,这个被引用的对象也占一定大小。myClass 实例所维护的引用的对象所占的大小,称为myClass实例的Retained heap size。我们这里仅讨论如何计算对象自身的大小,引用对象大小的计算方式可依此类推。
java对象内存可分为:头部 + 数据 + 对齐字节
±-----------------±-----------------±----------------- ±--------------+
| mark word | klass pointer | data (opt) | padding |
±-----------------±-----------------±------------------±--------------+
(1)头部大小(mark word + klass pointer)
- 在未开启 UseCompressedOops 的 64 位 JVM 上,对象头有 16 字节大小,即 8 字节的 mark word 和8 字节的引用。
- 在开启 UseCompressedOops 的 64 位机器上,引用成了 4 字节,一共 12 字节。
(2)数据大小(data)
空对象不包含任何成员变量,其大小即对象头大小。若存在成员成员,为了内存紧凑,成员在内存中的排列和声明的顺序可能不一致,这样才能充分利用内存空间。这是因为在32位系统中,对象大小需要为4byte(32位)的整数倍,而在64位的系统中,对象需要为8byte(64位)的整数倍。如下例子:
class MyClass {byte a;int c;boolean d;long e;Object f;
}
其内存布局为:
值得一提的是,数组对象和普通对象存在一点小区别:数组多一个记录数组长度的 int 类型(4byte)