Java对象结构

由对象头、对象体、对齐字节三部分组成,其结构如下图:
Java对象结构.jpg

对象头

Mark Word

概述

Mark Word(标记字),用于存储自身运行时的数据,例如GC标志位、哈希码、锁状态等信息。

结构信息
结构图
image.png
9a9ed258afe6ee392a49a996928eafa.jpg

详细介绍

:以下是64位JVM的 Mark Word
lock:锁状态标记位,占两个二进制位,由于希望用尽可能少的二进制位表示尽可能多的信息,因此设置了lock标记。该标记的值不同,整个Mark Word表示的含义就不同
biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。

biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。 6d07a9a64b5a74e80f53a139943b8a6.jpg

age:4位的Java对象分代年龄。在GC中,对象在Survivor区复制一次,年龄就增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,因此最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
identity_hashcode:31位的对象标识HashCode(哈希码)采用延迟加载技术,当调用Object.hashCode()方法或者System.identityHashCode()方法计算对象的HashCode后,其结果将被写到该对象头中。当对象被锁定时,该值会移动到Monitor(监视器)中。
thread:54位的线程ID值为持有偏向锁的线程ID。
epoch:偏向时间戳
ptr_to_lock_record:占62位,在轻量级锁的状态下指向栈帧中锁记录的指针。
ptr_to_heavyweight_monitor:占62位,在重量级锁的状态下指向对象监视器的指针。

Class Pointer

Class Pointer(类对象指针),用于存放方法区Class对象的地址,虚拟机通过这个指针来确定这个对象是哪个类的实例。

Array Length

Array Length(数组长度),是一个可选字段,当前对象是Java数组,则该字段必须有,记录数组长度的数据;当前对象不是一个Java数组,则该字段不存在。

对象体

对象体包含对象的实例变量(成员变量),用于成员属性值,包括父类的成员属性值。这部分内存按4字节对齐。

对其字节

对齐字节也叫作填充对齐,其作用是用来保证Java对象所占内存字节数为8的倍数HotSpot VM的内存管理要求对象起始地址必须是8字节的整数倍。对象头本身是8的倍数,当对象的实例变量数据不是8的倍数时,便需要填充数据来保证8字节的对齐。

内置锁

内置锁的4种状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态

无锁状态

概述

Java对象刚创建时没有任何线程来竞争,此时该对象处于无锁状态(无线程竞争它),这时偏向锁标识位是0,锁状态是01。无锁状态下对象的 Mark Word 如图所示:
8a6fff4bd31670cfcb1514df2cbd447.jpg

偏向锁状态

概述

偏向锁是指一段同步代码一直被同一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。如果内置锁处于偏向状态,当有一个线程来竞争锁时,先用偏向锁,表示内置锁偏爱这个线程,这个线程要执行该锁关联的同步代码时,不需要再做任何检查和切换。偏向锁在竞争不激烈的情况下效率非常高。
偏向锁状态的 Mark Word 会记录内置锁自己偏爱的线程ID,内置锁会将该线程当作自己的熟人。偏向锁状态下对象的 Mark Word 如图所示a3a4918e578d3e9438009dd8566e4cc.jpg

核心原理

如果不存在线程竞争的一个线程获得了锁,那么锁就进入偏向状态,此时 Mark Word 的结构变为偏向锁结构,锁对象的锁标志位(lock)被改为01,偏向标志位(biased_lock)被改为1,然后线程的ID记录在锁对象的 Mark Word 中(使用CAS操作完成)。

作用

消除无竞争情况下的同步原语,进一步提升程序性能。在没有锁竞争的场合,偏向锁有很好的优化效果;一旦有第二条线程需要竞争锁,则偏向模式立即结束,进入轻量级锁的状态。

缺点

锁对象时常被多个线程竞争,偏向锁就是多余的,并且其撤销的过程会带来一些性能开销。

撤销和膨胀

撤销

偏向锁撤销的过程:

  1. 在一个安全点停止拥有锁的线程。
  2. 遍历线程的栈帧,检查是否存在锁记录。如果存在锁记录,就需要清空锁记录,使其变成无锁状态,并修复锁记录指向的Mark Word,清除其线程ID。
  3. 将当前锁升级成轻量级锁。
  4. 唤醒当前线程。

    注:某些临界区存在两个及两个以上的线程竞争,那么偏向锁反而会降低性能。在这种情况下,可以在启动JVM时就把偏向锁的默认功能关闭。

撤销的条件:

  1. 多个线程竞争偏向锁。
  2. 调用偏向锁对象的hashcode()方法或者System.identityHashCode()方法计算对象的HashCode之后,将哈希码放置到Mark Word中,内置锁变成无锁状态,偏向锁将被撤销。
    膨胀
    如果偏向锁被占据,一旦有第二个线程争抢这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到内置锁偏向状态,这时表明在这个对象锁上已经存在竞争了。JVM检查原来持有该对象锁的占有线程是否依然存活,如果挂了,就可以将对象变为无锁状态,然后进行重新偏向,偏向为抢锁线程。
    如果JVM检查到原来的线程依然存活,就进一步检查占有线程的调用堆栈是否通过锁记录持有偏向锁。如果存在锁记录,就表明原来的线程还在使用偏向锁,发生锁竞争,撤销原来的偏向锁,将偏向锁膨胀(INFLATING)为轻量级锁。

    轻量级锁状态

    概述

    当有两个线程开始竞争这个锁对象时,情况就发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象,锁对象的Mark Word就指向哪个线程的栈帧中的锁记录。轻量级锁状态下对象的 Mark Word 如图所示6b52118c00277598f153967a18075f4.jpg

    目的

    尽可能不动用操作系统层面的互斥锁,因为其性能比较差。线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁地阻塞和唤醒对CPU来说是一件负担很重的工作。

    分类

    普通自旋锁和自适应自旋锁
    普通自旋锁
    指当有线程来竞争锁时,抢锁线程会在原地循环等待,而不是被阻塞,直到那个占有锁的线程释放锁之后,这个抢锁线程才可以获得锁。

    注:锁在原地循环等待的时候是会消耗CPU的,就相当于在执行一个什么也不干的空循环。轻量级锁适用于临界区代码耗时很短的场景,这样线程在原地等待很短的时间就能够获得锁了。 默认自旋的次数为10次,可以通过 -XX:PreBlockSpin 选项来进行更改

自适应自旋锁

等待线程空循环的自旋次数并非是固定的,而是会动态地根据实际情况来改变自旋等待的次数,自旋次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
自适应自旋锁的原理:

  1. 如果抢锁线程在同一个锁对象上之前成功获得过锁,JVM就会认为这次自旋很有可能再次成功,因此允许自旋等待持续相对更长的时间。
  2. 如果对于某个锁,抢锁线程很少成功获得过,那么JVM将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。

    重量级锁状态

    概述

    重量级锁会让其他申请的线程之间进入阻塞,性能降低。重量级锁也叫同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,该监视器对象用集合的形式来登记和管理排队的线程。重量级锁状态下对象的 Mark Word 如图所示:
    a96c2cad81b533d9b85fca13a255563.jpg

    核心原理

    JVM中每个对象都会有一个监视器,监视器和对象一起创建、销毁。监视器相当于一个用来监视这些线程进入的特殊房间,其义务是保证(同一时间)只有一个线程可以访问被保护的临界区代码块。

    监视器特点
  3. 同步。监视器所保护的临界区代码是互斥地执行的。一个监视器是一个运行许可,任一线程进入临界区代码都需要获得这个许可,离开时把许可归还。

  4. 协作。监视器提供Signal机制,允许正持有许可的线程暂时放弃许可进入阻塞等待状态,等待其他线程发送Signal去唤醒;其他拥有许可的线程可以发送Signal,唤醒正在阻塞等待的线程,让它可以重新获得许可并启动执行

    监视器组成

    Hotspot 虚拟机中,监视器是由C++类ObjectMonitor实现的,ObjectMonitor类定义在ObjectMonitor.hpp文件中,其构造器代码 ```cpp //Monitor结构体 ObjectMonitor::ObjectMonitor() {

    _header = NULL;
    _count = 0;
    _waiters = 0,

    //线程的重入次数 _recursions = 0;
    _object = NULL;

    //标识拥有该Monitor的线程 _owner = NULL;

    //等待线程组成的双向循环链表 _WaitSet = NULL;
    _WaitSetLock = 0 ;
    _Responsible = NULL ;
    _succ = NULL ;
    //多线程竞争锁进入时的单向链表 cxq = NULL ; FreeNext = NULL ;

    //_owner从该双向循环链表中唤醒线程节点 _EntryList = NULL ; _SpinFreq = 0 ;
    _SpinClock = 0 ;
    OwnerIsThread = 0 ;

} ``` ObjectMonitor的Owner(_owner)、WaitSet(_WaitSet)、Cxq(_cxq)、EntryList(_EntryList)这几个属性比较关键。
ObjectMonitor的WaitSet、Cxq、EntryList这三个队列存放抢夺重量级锁的线程,而ObjectMonitor的Owner所指向的线程即为获得锁的线程。

内置锁的对比

synchronized的执行过程

  1. 线程抢锁时,JVM首先检测内置锁对象Mark Word中的biased_lock(偏向锁标识)是否设置成1,lock(锁标志位)是否为01,如果都满足,确认内置锁对象为可偏向状态。
  2. 在内置锁对象确认为可偏向状态之后,JVM检查MarkWord中的线程ID是否为抢锁线程ID,如果是,就表示抢锁线程处于偏向锁状态,抢锁线程快速获得锁,开始执行临界区代码。
  3. 如果Mark Word中的线程ID并未指向抢锁线程,就通过CAS操作竞争锁。如果竞争成功,就将Mark Word中的线程ID设置为抢锁线程,偏向标志位设置为1,锁标志位设置为01,然后执行临界区代码,此时内置锁对象处于偏向锁状态。
  4. 如果CAS操作竞争失败,就说明发生了竞争,撤销偏向锁,进而升级为轻量级锁。
  5. JVM使用CAS将锁对象的Mark Word替换为抢锁线程的锁记录指针,如果成功,抢锁线程就获得锁。如果替换失败,就表示其他线程竞争锁,JVM尝试使用CAS自旋替换抢锁线程的锁记录指针,如果自旋成功(抢锁成功),那么锁对象依然处于轻量级锁状态。
  6. 如果JVM的CAS替换锁记录指针自旋失败,轻量级锁就膨胀为重量级锁

    三种内置锁的对比

    | 锁 | 优点 | 缺点 | 使用场景 | | —- | —- | —- | —- | | 偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问的临界区场景 | | 轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应时间 | 抢不到锁竞争的线程使用CAS自旋等待,会消耗CPU | 所占用时间很短,吞吐量低 | | 重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 锁占用时间较长,吞吐量高 |