1. 对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3个区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。
1.1 对象头
对象头包含2部分的信息:第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,官方称它为Mark Word。
Mark Word会复用自己的存储空间。例如,在32位的HotSpot虚拟机中,如果对象处于未被锁定的状态下,则25bit用于存储对象哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0。其状态下对象的存储内容如下:
JVM一般是这样使用锁和Mark Word的:
当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。
对象头的另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个Java数组,那么对象头重还必须有一块用于记录数组长度的数据。该数据在32位和64位JVM中长度都是32bit。
1.2 实例数据
实例数据部分是对象真正存储的有效信息。这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。HotSpot的分配策略中,相同宽度的字段总是被分配在一起,在这个前提下,在父类中定义的变量会出现在子类之前。
1.3 填充字段
这个字段不是必然存在的,仅仅是占位符。HotSpot虚拟机要求对象起始地址必须是8字节的整数倍。对象头部分是8字节的整数倍。所以,当对象实例数据的部分没有对齐时候,就需要通过对齐填充来补全。
2. 对象的访问定位
对于reference类型的对象的访问,目前主流的访问方式有使用句柄和直接指针两种。
-
如果使用句柄,Java堆中将会划分出来一块内存作为句柄池,reference中存储的就是对象的句柄地址
-
直接指针访问,reference中存储的直接就是对象地址。
对比
使用句柄进行访问最大的好处就是reference中存储的是稳定的句柄地址,在对象被移动时智慧改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针的好处是速度更快,节省了一次指针定位的事件开销,HotSpot虚拟机使用的是直接指针的方式进行对象的访问。
3. 引用
共同学习,写下你的评论
评论加载中...
作者其他优质文章