Java对象内存结构 鉴于我之前学习C/C++,Java中有一件事始终困扰着我,就是Java中缺少一个计算对象占用内存大小的方法。C++提供了sizeof运算符让你能计算基本类型和一个给定类型的对象占用内存的大小。这个操作符在C和C++中对于像指针运算,内存拷贝和IO这类操作都非常有用。 Java没有类似的操作符。实际上,Java并不需要这样的操作符。在Java中基本类型的大小是语言描述的,而在C和C++中是与平台相关的。Java有它自己的一套为序列化而设计的IO体系结构。而且Java没有指针运算和大内存拷贝,因为Java就没有指针这个概念。 即便如此,Java开发人员有时候还是好奇一个Java对象到底占用了多大的内存。事实证明,答案并不那么简单。 首先需要弄清楚的是“浅大小”和“深大小”的区别。一个对象的“浅大小”是指这个对象本身所占用内存空间的大小,并不考虑它所关联对象的大小。另一方面,“深大小”不仅考虑了这个对象的“浅大小”还把这个对象所有引用的对象的大小迭代的计算在内。大部分情况下,你都希望知道一个对象的“深大小”,但是为了知道它,你得首先先学会怎么计算“浅大小”,这正是我在这儿要讨论的。 现在有一个复杂的情况就是Java对象在运行时的内存结构并没有在虚拟机中被严格的规范,这也就意味着不同的虚拟机提供者可以根据他们自己的意愿去实现虚拟机。这样造成的后果就是你定义的同一个类型的实例在不同的虚拟机中会占用不同的内存空间。然而普遍的,包括我自己在内,使用的都是Sun HtoSpot虚拟机,这使得事情变得简单。剩余部分的讨论都是针对32位的Sun JVM。我将列出几个规则来说明Java虚拟机在内存中怎么组织对象的布局。 没有实例属性的类的内存布局 在Sun JVM中,所有的对象(除了数组)都有两个字的头部,第一个字包含对象的标识,哈希码以及一些标志信息,比如锁的状态,年龄(注:经过了几次gc)。第二个字是指向该对象类型的应用。另外任何对象都对齐到一个8字节的粒度。这也就是第一个规则或者说对象内存布局: 规则一:所有的对象都对齐到一个8字节的粒度。 现在我知道了如果调用new Object(),我们只需要将堆里面的8个字节分配给对象的头部,因为Object类没有任何的属性。 继承Object的类的内存布局 在8个字节的头部之后紧接着是类的属性。属性总是根据它们自己类型的大小进行对齐。举个例子,int被对齐到一个4字节的粒度,long被对齐到一个8字节的粒度。这样做是出于效率的原因:通常如果字被对齐到一个4字节的粒度,那么从内存中读取一个4字节的字到处理器的寄存器中将会很高效。 为了节省一些内存,Sun JVM没有按照属性在类中声明的顺序进行布局,而是按照下面的顺序在内存中进行组织的:
这个策略能很好的优化内存的使用。例如,假设你定义了下面的一个类: class MyClass { byte a; int c; boolean d; long e; Object f; } 如果Java虚拟机没有调整属性的顺序,对象的内存布局会是如下所示: [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 注意到,因为需要对其,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个字节的内存空间。 这就是对象内存布局的规则二 规则二:类的属性按照这样的数序进行排序:首先是long和double,然后int和float,然后char和short,然后byte和boolean,最后是引用。每个属性都按照它们自己的粒度进行对其。 现在我们知道了怎么计算一个直接继承自Object的类的对象占用内存的大小。一个实际的例子是java.lang.Boolean类。下面是它的内存布局: [HEADER: 8 bytes] 8 [value: 1 byte ] 9[padding: 7 bytes] 16 一个Boolean类的实例占用了16个字节的内存!是不是很惊讶?(注意最后填充的字节是为了对齐8字节粒度) 其他类的子类的内存布局 接下来对于那些继承了父类的类java虚拟机有三个规则来组织其内存结构。对象内存布局的规则三如下: 规则三:属于不同类层次的字段永远不要混合在一起。父类的字段放在第一位,遵守规则二,其次是子类的字段。 举例如下: 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字节边界。 举例如下: class A { byte a;}class B { byte b;}[HEADER: 8 bytes] 8[a: 1 byte ] 9[padding: 3 bytes] 12[b: 1 byte ] 13[padding: 3 bytes] 16 注意字段a后面的3个字节的填充是为了对齐到4字节的粒度。填充的空间会丢失而且不能被B类的字段所使用。 如果子类的第一个字段是long或者double并且父类没有对齐到8字节的边界,最后的这个规则将被使用来节省空间。 规则五:如果子类的第一个字段是long或者double并且父类没有对齐到8字节的边界,Java虚拟机将打破规则二并且尝试按照:首先int,然后short,然后byte,最后引用的顺序将字段放到子类内存空间开始的地方,直到填充整个空白。 举例如下: class A { byte a;}class B { 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类结束,Java虚拟机打破规则二将一个short和一个byte放在了long的前面,从而节省了可能会浪费的三四个字节。 数组的内存布局 与普通对象不同,数组有一个额外的头部,该头部用来储存数组的长度。接着是数组的元素,数组作为一个对象,同样也需要对齐到一个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 内部类的内存布局 非静态内部类有一个额外的“隐藏”字段用来保存指向外部类的引用。这个字段是一个普通的引用,它遵从引用类型的内存布局的规则。所以,内部类会有额外的4个字节的代价。 |
|( 京ICP备09078825号 )
GMT+8, 2024-11-23 21:59 , Processed in 0.128622 second(s), 45 queries .