zoukankan      html  css  js  c++  java
  • Java对象头学习指南——实战篇

    前言

    通过上一篇博客 here 的讲述,我们已经找到了有价值的“官方文档”。

    本文自然是要来用 jol-core 来实战和深入理解 synchronized 锁和 Java 对象头之间的 “纠葛” 了。

    Object header is the common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.)

    Object header consists of two words. Mark word is the first word of every object header. Klass pointer the second word of every object header.

    Oop is an object pointer.

    上文截取自 Hotspot 的术语表 link here

    由垃圾收集器管理的堆对象,都包含对象头。每个对象指针指向一个对象头。

    对象头主要由 Mark word 和 Klass pointer 构成。如果是数组对象,还会有数组长度字段。

    对象头在对象结构中位于最开始的位置。

    关于 “word”

    通常我们所说的 64 位操作系统和 32 位操作系统中的 64 和 32 指的是 CPU 中的寄存器(通用)的字长,字长就是一个字的位数。

    CPU 寄存器是比内存存取速度更快的硬件,在程序运行时,数据会从内存中加载到寄存器中,然后 CPU 再分配时间片处理寄存器中的数据。

    我的电脑是 64 位操作系统,因此字长就是 64,即 8 字节。

    无锁对象头

    写一个 Java main 函数,代码如下(如果不了解 Java 日志体系,可以将 log.debug 替换成 System.out.println 也是可以的。)

    Object lock = new Object();
    log.debug("================ 无锁 ================");
    log.debug(ClassLayout.parseInstance(lock).toPrintable());
    

    打印结果如下图:

    关闭指针压缩

    最开始的 8 个字节,就是“ first word” 了,那么绿色部分自然就是 Mark word 了。

    但是,问题来了,接下来的 8 个字节,即红色框内的部分,似乎并不全都表示 klass pointer 啊?

    原因是 “指针压缩” 默认开启,我们可以通过 JVM 参数进行调整:

    在 Intellij IDEA 中,Run -> Edit Configuration... -> VM Options 配置参数 -XX:-UseCompressedOops

    关闭指针压缩之后,运行结果如下:

    unUseCompressedOops

    现在来看,正好就印证了第一个字表示 Mark Word,第二个字表示 Klass Pointer。我们继续集中注意力在 Mark Word 上面。

    Mark word

    mark word : Usually a set of bitfields including synchronization state and identity hash code.

    Mark word 中就包含我们期望的同步状态,同时还包含hashCode。但是,怎么分配 bit 位来表示不同的部分?

    hash code

    我们观察到在之前的运行结果图中,bit 位除了一个扎眼的 1,其他全是 0 ,根本没有 hashCode。

    我们发现一个新的问题,在打印一个普通对象的对象头时,为什么 Mark word 中没有 hash code?

    原因:Object#hashCode() 这是个 native 方法,调用之后,对象头 Mark word 才会有 hash code 值。

    所以,我先调用 hashCode(),再打印对象头。为了方便查看我已经把 hashCode 由 10 进制转化为 16 进制。

    Object obj = new Object();
    log.debug("对象 {} 的 hashCode: {}", obj, Integer.toHexString(obj.hashCode()));
    log.debug(ClassLayout.parseInstance(obj).toPrintable());
    

    接着我们来观察一下结果:

    我们现在可以看到 hash code 值了,但是为什么对象头的 hashCode 和我们常用的 hashCode 顺序相反呢?

    原因:存储器中对数据的存储是以字节(Byte)为基本单位的,因此,字(Word)和半字(Half-Word)在存储器中就有两种次序,分别称为:大端模式(Big Endian)和小端模式(Little Endian)。

    另外,需要注意的几点是:

    1. 数据在寄存器中都是以大端模式次序存放的。

    2. 对于内存中以小端模式存放的数据。CPU存取数成时,小端和大端之间的转换是通过硬件实现的,没有数据加载/存储的开销。

    另外,按照人类的一般习惯,都是认为左边以及上边是开始的位置,从阅读顺序上来看,左边和上边就是低地址,假如和十进制数一样,万-千-百-十-个,这种就是大端模式,大端也就是高位在前,低位在后。小端则相反。

    我们再看一眼 jdk8/hotspot/file/vm/oops/markOop.hpp 上关于普通对象的注释,画出下面这幅图:

    我们可以把结果从小端模式转换成大端模式

    现在我们可以截取出 hash code 的部分了

    二进制即 0101001 01000100 01001101 01110101二进制数转换 16 进制数,高位补零不受影响,因此就得到十六进制数 29444d75 。

    锁标记

    无锁和偏向锁

    无锁和偏向锁本质上都是没有上锁的状态。所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程。lock:01,无锁的 biased_lock: 0,偏向锁 biased_lock: 1

    >> 实战代码:
    
    Thread thread = new Thread(() -> {
          Object lock = new Object();
          log.debug(ClassLayout.parseInstance(lock).toPrintable());
          synchronized (lock) {
                log.debug(ClassLayout.parseInstance(lock).toPrintable());
          }
    });
    thread.start();
    thread.join();
    

    普通对象(无锁):

    Normal Object

    BiasedLockingStartupDelay

    然后,我们来看一下打印的第二段日志:

    出现一个新的疑问:偏向锁明明是 101,为什么成了 000? 000 应该是轻量锁才对。

    我们通过配置 JVM 参数 -XX:+PrintFlagsFinal 看到一些 JVM 标志位信息:

    BiasedLockingStartupDelay 表示系统启动几秒钟后启用偏向锁,单位毫秒。默认为4秒,原因在于,系统刚启动时,一般数据竞争是比较激烈的,此时启用偏向锁会降低性能。

    创建对象时,会决定锁是否可以偏向

    • 假如对象的在 JVM 内存堆上晚于 BiasedLockingStartupDelay 的时长
    >> 实战代码:
    
    public static void main(String[] args) throws InterruptedException {
            // 睡眠 4 秒
            TimeUnit.SECONDS.sleep(4);
            Object lock = new Object();
            log.debug(ClassLayout.parseInstance(lock).toPrintable());
            synchronized (lock) {
                log.debug(ClassLayout.parseInstance(lock).toPrintable());
            }
    }
    

    • 假如对象的在 JVM 内存堆上早于 BiasedLockingStartupDelay 的时长
    >> 实战代码:
    
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        // 睡眠 4 秒
        TimeUnit.SECONDS.sleep(4);
        log.debug(ClassLayout.parseInstance(lock).toPrintable());
        synchronized (lock) {
            log.debug(ClassLayout.parseInstance(lock).toPrintable());
        }
    }
    

    PS: 如果想要修改偏向锁启动延迟时长,可以考虑使用以下 JVM 参数:

    -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=2000

    轻量级锁

    • When the lock is biased toward a given thread, locking and unlocking can be performed by that thread without using atomic operations.
      当锁对象已经偏向某个线程,加锁和解锁由该线程执行,而不需要进行 CompareAndSet 的原子性操作。

    • When a lock's bias is revoked, it reverts back to the normal locking scheme described below.
      当锁的偏向再次触发,它恢复到下面描述的正常加锁方案。

    实战代码
    
    public static void main(String[] args) throws InterruptedException {
        final Object object = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (object) {
                log.debug("------------- {} 获得偏向锁 -------------", Thread.currentThread().getName());
                log.debug(ClassLayout.parseInstance(object).toPrintable());
            }
        }, "t1");
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        log.debug("============= 2s 以后启动 t2 =============");
        Thread t2 = new Thread(() -> {
            synchronized (object) {
                log.debug("------------- {} 获得轻量级锁 -------------", Thread.currentThread().getName());
                log.debug(ClassLayout.parseInstance(object).toPrintable());
            }
        }, "t2");
        t2.start();
    }
    

    注意:设置 VM Options:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

    线程 t1 线程中,synchronized 关键字使得锁发生偏向,对象头 Mark Word 最低的3位变为 101,同时把 Java Thread 对象的指针存储到 lock 对象的对象头 Mark Word 关键字中。

    线程 t2 在 t1 线程之后启动,两线程交替执行,但是在 t2 线程中调用 synchronized(lock) {} ,再次发生偏向,此时升级到轻量级锁。对象头 Mark word 最低3位变为 000

    重量级锁

    当线程 t1 和 t2 之间并行执行,发生竞争锁的情况,轻量级锁会膨胀为重量级锁。此时会创建一个 C++ 层面的 MonitorObject 对象,而 object 对象头中也会保存该对象的指针。

    -XX:PreInflateSpin=20 可以修改自旋次数,默认值是10次。超过该次数,轻量级锁会升级为重量级锁。

    Lock Record

    • 轻量级锁CAS操作之前堆栈与对象的状态:

    Lock Record 一开始是由 Displaced hdr 和 owner 组成。

    • 轻量级锁CAS操作之后堆栈与对象的状态:

    以下 lock 对象指的是 synchronized(lock) {} 括号内的这个对象

    lock 升级为轻量级锁,对象头 Mark Word 变成了 pointer | 00,pointer 指针指向栈上的 Lock Record

    lock 对象创建时的 Mark Word 被存入栈中。

    栈上的 owner 指针指向 lock 对象。

    总结

    • 调用 hashCode(),无法升级为偏向锁,直接升级为轻量级锁

    • +XX:BiasedLockingStartupDelay=n 表示系统启动 n 毫秒之后,启用偏向锁

    • +XX:PreInflateSpin=n 表示自旋 n 次之后,轻量锁升级为重量级锁

    [ptr | 00] locked ptr points to real header on stack
    [header | 0 | 01] unlocked regular object header
    [ptr | 10] monitor inflated lock (header is wapped out)
    [ptr | 11] marked used by markSweep to mark an object not valid at any other time

    • 轻量级锁,CAS 是在栈上进行操作的

    C++ 中的几个类,以及在 markOops.hpp 中的对应方法

    class method Memo
    **ObjectMonitor ** monitor() 重量级锁,对象头中指针指向的监视器对象
    **JavaThread ** biased_locker() 偏向锁,对象头中的指针指向的线程对象

    参考博客

  • 相关阅读:
    构建之法阅读笔记2
    2020.2.10
    2020.2.9
    2020.2.8
    2020.2.7
    2020.2.6
    2020.2.5
    学习进度——第十三周
    PHPStorm+XAMPP开发php项目
    第一阶段成果建议
  • 原文地址:https://www.cnblogs.com/kendoziyu/p/java-object-header-guide-1.html
Copyright © 2011-2022 走看看