- 对象在内存中的存储可以分为三个区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。
头对象
-
对于不同的对象类型,虚拟机存储的长度也不同。如果对象是数组类型,则虚拟机用3个字宽存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节,既32bit。入下图所示:
长度 内容 说明 32/64bit Mark Word 存储对象的hashcode和锁信息等 32/64bit Class MetaData Address 存储到对象类型数据的指针 32/64bit Array length 数组长度(如果当前是数组)
Mark Word
Mark Word存储的是对象自身的运行数据,如哈希码,GC分代年龄,锁状态标记,偏向锁ID,偏向时间戳等。
由于32bit,64bit储存的数据不够多,为了存储更多信息,Mark Word被设计成非固定的存储结构,其存储的数据会随着锁标志位的变化而变化。
-
32bit虚拟机中的Mark Word存储结构
-
64bit虚拟机中的Mark Word存储结构
轻量级锁和偏向锁是Java 6 对 synchronized 锁进行优化后新增加的,这里并不进行详细介绍。
类型指针
- 用来指向对象对应的Class对象(其对应的元数据对象)的内存地址。在32位系统占4字节,在64位系统中占8字节;
Array Length
- 如果对象是个数组的话,对象头中就必须要有一块用于记录数组长度的数据,不然虚拟机无法确定数组的大小。32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启
+UseCompressedOops
选项(开启指针压缩),该区域长度也将由64位压缩至32位。32位HotSpot VM是不支持UseCompressedOops参数的,只有64位HotSpot VM才支持。
实例数据
- 实例数据是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是自己本身定义的,都要记录下来。
- 这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。
- HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。如果 CompactFields参数值为true(默认为true),那子类之中占空间较小的变量也可能会插入到父类变量的空隙之中(JVM按8位对齐)。
对齐填充
- 对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。
- HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。因此,对于普通对象来说,其对象头的所占的字节数是8的整数倍(32bit下是1倍,64bit下是2倍)。此时则出现在实例数据部分可能没有对齐,就需要对齐填充来补全。假如是数组对象,由于对象头多了Array Length域,因此可能对象头也需要对齐填充。
CompressedOops(指针压缩)
通常64位JVM消耗的内存会比32位的最多会多用1.5倍,这是因为对象指针在64位架构下,对象指针长度会翻倍。 对于那些将要从32位平台移植到64位的应用来说,平白无辜多了1/2的内存占用,这是开发者不愿意看到的。从JDK 1.6 update14开始,64 bit JVM正式支持了 -XX:+UseCompressedOops 这个可以压缩指针,起到节约内存占用的新参数。
-
OOP = “ordinary object pointer” 普通对象指针。 启用CompressOops后,会压缩的对象:
每个Class的属性指针(静态成员变量)
每个对象的属性指针
普通对象数组的每个元素指针
压缩也不是万能的,针对一些特殊类型的指针,JVM是不会优化的。 比如指向PermGen的Class对象指针,本地变量,堆栈元素,入参,返回值,NULL指针不会被压缩。
原理:32位内最多可以表示4GB,64位地址分为堆的基地址+偏移量,当堆内存<32GB时候,在压缩过程中,把偏移量/8后保存到32位地址。在解压再把32位地址放大8倍,所以启用CompressedOops的条件是堆内存要在4GB*8=32GB以内。
零基压缩是针对压解压动作的进一步优化。它通过改变正常指针的随机地址分配特性,强制从零开始做分配(需要OS支持),进一步提高了压解压效率。要启用零基压缩,你分配给JVM的内存大小必须控制在4G以上,32G以下。