本文转载自微信公众号《小菜也牛》,作者贾健。转载本文请联系小菜一牛公众号。今天讲一些抽象的东西——对象头,因为在学习过程中,发现很多地方都和对象头的知识点有关,比如JDK中的同步锁优化,JVM中的对象年龄升级等。深入理解这些知识的原理,有必要了解对象头的概念,可以为后面的synchronized原理和JVM知识的分享做准备。对象内存构成Java中new关键字创建的类的实例对象。对象存储在内存堆中,并为其分配一个内存地址。你有没有想过下面的问题:这个实例对象是如何存在于内存中的?一个Object对象在内存中占用多少?对象中的属性在内存中是如何分配的?在JVM中,一个Java对象存储在堆中时,由以下三部分组成:对象头:包括堆对象的布局、类型、GC状态、同步状态和标识哈希码等基本信息。Java对象和vm内部对象都有共同的对象头格式。实例数据(InstanceData):主要存储类的数据信息、父类的信息、对象字段的属性信息。对齐填充(Padding):对于字节对齐来说,填充数据不是必需的。对于对象头,我们可以在Hotspot官方文档(如下)中找到它的描述。可以发现,它是Java对象和虚拟机内部对象的通用格式,由两个词(计算机术语)组成。另外,如果对象是Java数组,那么对象头中必须有一段数据用来记录数组的长度,因为虚拟机可以通过普通Java的元数据信息来判断Java对象的大小对象,但是从数组的元数据信息中无法确定数组的大小。它提到对象头由两个词组成。这两个词是什么?我们还是在上面的Hotspot官方文档中查找,可以发现名词还有另外两个定义,分别是markword和klasspointer。从中可以找到对象头中的两个词:第一个词是markword,第二个词是klasspointer。MarkWord用于存储对象本身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。32位JVM中MarkWord的长度为32bit,64位JVM中为64bit。我们打开openjdk的源码包,对应路径/openjdk/hotspot/src/share/vm/oops,MarkWord对应C++代码markOop.hpp,从注释中可以看到它们的组成,所有代码都在这文章基于Jdk1。8位和64位操作系统。MarkWord在不同的锁定状态下存储的内容是不同的。在32位JVM和64位JVM中都是这样存储的。虽然在不同位数的JVM中它们的长度不同,但是基本的组成内容是一样的。的。锁标志(lock):区分锁状态,11表示对象正在等待GC回收,只有最后2个锁标志(11)有效。biased_lock:锁是否偏向。由于普通锁和偏向锁的锁ID都是01,所以没法区分。这里引入一个1位的偏向锁ID。Generationalage(年龄):表示对象被GC的次数。当数量达到阈值时,对象将被转移到老年代。对象的Hashcode(散列):运行时调用System.identityHashCode()进行计算,延迟计算,并将结果赋值在这里。对象上锁后,31位的计算结果不足以表示,hashcode会传给Monitor进行偏向锁、轻量级锁、重度锁。偏向锁的线程ID(JavaThread):在偏向模式下,当一个线程持有一个对象时,该对象会被设置为该线程的ID。在后续操作中,不需要再去尝试获取锁。epoch:Biasedlock在CAS锁操作过程中,biasedflag表示对象更偏向于哪个锁。ptr_to_lock_record:在轻量级锁状态下,指向栈中锁记录的指针。当锁获取没有竞争时,JVM使用原子操作而不是OS互斥锁。这种技术称为轻量级锁定。在轻量级锁的情况下,JVM通过CAS操作在对象的headerword中设置一个指向锁记录的指针。ptr_to_heavyweight_monitor:在重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,轻量级锁必须升级为一个监视器来管理等待线程。在重量级锁的情况下,JVM在对象的ptr_to_heavyweight_monitor中设置一个指向Monitor的指针。KlassPointer是类型指针,是对象指向其类元数据的指针。虚拟机使用这个指针来确定对象是哪个类的实例。实例数据如果对象有属性字段,这里会有数据信息。如果对象没有属性字段,这里就没有数据。根据字段类型,占用不同的字节。比如boolean类型占1个字节,int类型占4个字节等;对齐数据对象可以有或没有对齐数据。默认情况下,Java虚拟机堆中对象的起始地址需要对齐到8的倍数。如果一个对象使用小于8N字节,需要填充,以填满对象头和实例之后的剩余空间数据占用内存。如果对象头和实例数据已经占用了JVM分配的内存空间,那么就不需要进行对齐填充。所有对象分配的总字节SIZE需要是8的倍数,如果前面的对象头和实例数据占用的总SIZE不满足要求,会通过对齐数据的方式进行填充。为什么要对齐数据?字段内存对齐的原因之一是让字段只出现在同一CPU的缓存行中。如果字段未对齐,则可能存在跨越缓存行的字段。也就是说,字段的读取可能需要替换两个缓存行,而字段的存储可能同时污染两个缓存行。这两种情况都不利于程序的执行效率。实际上,填充它的最终目的是为了高效的计算机寻址。至此,我们了解了对象在堆内存中的整体结构布局。如下图,Talk的概念是廉价的,showmecode是抽象的。如果你说它是这样组成的,是真的吗?学习是需要持怀疑态度的,任何理论或概念只有经过自己的验证和实践才能被接受。幸运的是,openjdk为我们提供了一个工具包,可以用来获取对象信息和虚拟机信息。我们只需要引入jol-core依赖,如下
