java 对象内存结构

前言

HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志位,1Bit固定为0。

内存结构

java对象内存结构主要包含:对象头,各属性值,填充Padding(用于确保对象是8字节的倍数)。
java对象内存结构

在java中,32位jvm下指针占4字节,64位jvm下占8字节(但如果启用指针压缩,则占4字节),HotSpot默认的内存分配顺序为longs/doubles(8字节)、ints(4字节)、 shorts/chars(2字节)、bytes/booleans(1字节)、oops(Ordinary Object Pointers, 即对象指针),相同宽度的字段总是被分配到一起,父类与子类的属性不能交叉混合存放。且确保对象是8字节对齐,即任意Java对象的内存长度为8字节的整数倍。

内存分配原则:

  • 先父类再子类,即优先分配父类里面的属性(基本数据类型及指针类型);
  • 先基本数据类型再指针类型,即优先分配基本数据类型的变量,然后才是指针类型;
  • 相同宽度的字段总是被分配到一起,父类与子类的属性不能交叉混合存放;

ps:为了保证父类与子类的属性不会交叉混合存放,原则2的分配是在满足原则1的前提条件下的。

java对象头长度 = _mark + _klass + 数组长度(4字节):

  • _mark域长度:32位jvm占4字节,64位jvm占8字节;
  • _klass域长度:32位jvm占4字节,64位jvm占8字节(启用指针压缩占4字节,如果堆内存大小超过32G,则启用指针压缩无效,还是占用8字节);
  • 如果是数组对象则在普通对象的基础上,还包含4字节,用于标记数组长度;

对象头在64位下是16字节,默认压缩后是12字节(数组对象则是16字节),对象头在32位下为8字节,数组对象则为12字节;
对象指针在32位占4字节,64位下占8字节(启用指针压缩则占4字节,如果堆内存大小超过32G,则启用指针压缩无效,还是占用8字节)

且JDK从6 update 23开始在64位系统上会默认开启压缩指针,在32位jvm中没有设置开启指针压缩的功能。

计算示例

示例1

示例2

总结

  • 为什么不用对象的内存绝对地址定位?

    java中获取对象中的属性值,是根据字段在Class中的偏移量来获取的(即根据相对地址获取字段属性值),因为在java堆中存在新生代,老年代等区别,对象是移动的,它的物理地址是变化的,所以要根据相对地址获取属性值。

  • 如何获取从对象中获取该Class对象的内存地址?

    SimpleClass simple = new SimpleClass();
    boolean isJvm32 = (UnsafeUtils.getUnsafe().addressSize() == 4);
    long classAddress = 0;
    if(isJvm32){
    // 在32位jvm下 _mark域占4字节,指针占4字节(所以是getInt)
    classAddress = UnsafeUtils.getUnsafe().getInt(simple, 4L);
    } else {
    // 64位jvm下 _mark域占8字节,指针在开启压缩并且堆内存小于32G占下4字节,未开启压缩占8字节
    if(启用指针压缩 && 堆内存小于32G){
    classAddress = UnsafeUtils.getUnsafe().getInt(simple, 8L);
    } else {
    classAddress = UnsafeUtils.getUnsafe().getLong(simple, 8L);
    }
    }
  • 如何判断当前jvm是32还是64位?

    使用Unsafe.addressSize()方法,返回4代表32位,返回8代表64位。

  • 为什么java对象是8字节对齐的?

    首先,CPU从L1级缓存中读取数据是以word为单位的,在32位处理器下word是4字节,在64位处理器下是8字节。而当时服务器领域大多都是64位的处理器,所以才设计的是8字节对齐,通过将java对象padding补长为word的整数倍,可以有效的提升CPU存取数据的性能;在Hotspot虚拟机中,不管是32位还是64位系统,java对象都是8字节对齐的(即都是8字节的整数倍)。在java中,并不强制要求处理器对long/double等8字节长变量的读写具备原子性,在32位处理器下,有可能会出现读取long/double变量高位及低位错乱的情况(注:之所以说是可能,是因为有些32位的处理器支持对8字节变量原子读写操作的指令),所以,为了保证对long/double变量的原子读写,要加上volatile关键字修饰。

  • 一个类所有实例所占用的内存大小是一样的。

参考链接

  1. http://www.infoq.com/cn/articles/jvm-hotspot
  2. http://blog.csdn.net/iter_zc/article/details/41822719