jvm · 2021-12-21 0

JVM中OOP-KLASS模型与synchronized锁

在 JVM 中,使用 OOP-KLASS 模型表示 JAVA 对象

一、对象创建

  1. jvm 在加载 class 时,会创建 instanceKlass ,表示其元数据,包括常量池、字段、方法等,存放在方法区。instanceKlass 是 jvm 中的数据结构。

  2. 在new一个对象时,jvm 创建 instanceOopDesc,来表示这个对象,存放在堆区;其引用,存放在栈区;它用来表示对象的实例信息。instanceOopDesc 对应 java 中的对象实例。

  3. Hospot 并不把 instanceKlass 暴露给 java,而会另外创建对应的 instanceOopDesc 来表示 java.lang.Class 对象,并将后者称为前者的"Java镜像",klass 持有指向 oop 引用 (_java_mirror便是该 instanceOopDesc 对 Class 对象的引用)。

  4. new 操作返回的 instanceOopDesc 类型指针(也叫元数据指针)指向 instanceKlass,instanceKlass 指向了对应的类型 Class 实例的 instanceOopDesc。

二、instanceOopDesc

JDK为1.8,因此是默认开启了指针压缩

下面通过修改VM参数,来开启或关闭指针压缩:
-XX:+UseCompressedOops // 开启指针压缩
-XX:-UseCompressedOops // 关闭指针压缩

对象保存在内存时,由三部分组成:

  1. 对象头
    1. Mark Word (标记字段)
      (标记字段用以存储 Java 虚拟机有关该对象的运行数据,如哈希码、GC 信息以及锁信息)
      在32位 JVM 中的长度是 32 bit,在64位 JVM 中长度是 64 bit (8字节)
    2. Klass Pointer (类型指针 指向类的指针)
      开启压缩是4字节,没有开启压缩是8字节
    3. 数组长度 (只有数组对象才有) (4字节)
  2. 实例数据
    存储对象的所有成员变量,static 成员变量不包括在内
  3. 对齐填充字节
    Java 对象的内存空间是8字节对齐的,因此总大小不是8的倍数时,会进行补齐

一个对象在内存中存储必须是8字节的整数倍

三、instanceKlass

klass 模型保存这 vtable, itable

vtable: 该类所有的函数(除了static, final)和 父类的函数虚拟表。

Itable: 该类所有实现接口的函数列表.

四、对象头

pom

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

测试

public class Student {

    private String name;

    private short age;

    private final static boolean alive = true;

    public Student() {
    }

    public Student(String name, short age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public short getAge() {
        return age;
    }

    public void setAge(short age) {
        this.age = age;
    }

}
Student student = new Student("zhang san", (short) 18);

// 无锁不可偏向(001)
@Test
public void test001() {
    System.out.println(String.format("Thread id: %s, Thread name: %s",
            Thread.currentThread().getId(),
            Thread.currentThread().getName()));
    System.out.println(student + " 十六进制哈希:" + Integer.toHexString(student.hashCode()));
    System.out.println(ClassLayout.parseInstance(student).toPrintable());
}

结果

前两行表示: Mark Word (标记字段) (8字节)
第三行表示: Klass Pointer (类型指针) (4字节)

基本上采用的都是小端存储,小端存储就是高地址存高字节,低地址存低字节。

所以 Mark Word 数据是:

00000000 00000000 00000000 00011000 10001000 11111111 00101100 00000001,即00 00 00 18 88 ff 2c 01

Thread id: 1, Thread name: main
com.example.jol.Student@1888ff2c 十六进制哈希:1888ff2c
com.example.jol.Student object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 2c ff 88 (00000001 00101100 11111111 10001000) (-1996542975)
      4     4                    (object header)                           18 00 00 00 (00011000 00000000 00000000 00000000) (24)
      8     4                    (object header)                           d8 11 01 f8 (11011000 00010001 00000001 11111000) (-134147624)
     12     2              short Student.age                               18
     14     2                    (alignment/padding gap)                  
     16     4   java.lang.String Student.name                              (object)
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 2 bytes internal + 4 bytes external = 6 bytes total

五、synchronized 锁

mark word在不同的状态下存储的信息:

mark-word

Java对象的锁状态一共有四种,级别从低到高依次为: 无锁(01) -> 偏向锁(01) -> 轻量级锁(00) -> 重量级锁(10).

但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。

1.偏向锁

偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了

当线程1访问代码块并获取锁对象时,会CAS在java对象头和栈帧中记录偏向的锁的threadID;
因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;
如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活;
如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

threadID不是当前线程的话,也会继续进行CAS操作的,一旦CAS失败,才会需要升级,如果成功了,还是执行同步代码块

2.轻量级锁

轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。

线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),
然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;

如果在线程1复制对象头的同时(在线程1进行CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2进行CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。

但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

sync000

3.重量级锁

重量级锁:如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞

重量级锁通过对象内部的监视器(ObjectMonitor)实现,其本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,成本非常高。

ObjectMonitor结构的c++源码:

  ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

ObjectMonitor 结构,其中有几个比较关键的属性:

  • _owner: 指定持有ObjectMonitor对象的线程
  • _WaitSet: 存放处于wait状态的线程队列
  • _EntryList: 存放处于等待锁block状态的线程队列
  • _recursions: 锁的重入次数
  • _count: 用来记录该线程获取锁的次数

当多个线程同时访问一段同步代码时,首先会进入 _EntryList 队列中,当某个线程通过竞争获取到对象的 monitor 后,monitor 会把 _owner 变量设置为当前线程,同时 monitor 中的计数器 _count 加 1,即获得对象锁。

若持有 monitor 的线程调用 wait() 方法,将释放当前持有的 monitor_owner 变量恢复为 null_count 自减 1,同时该线程进入 _WaitSet 集合中等待被唤醒。若当前线程执行完毕也将释放 monitor(锁)并复位变量的值,以便其他线程进入获取 monitor(锁)。

sync010-1

sync010-2

sync010-3

sync010-4

4.自旋锁

自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量

六、sychronized和ReentrantLock的区别

  1. sychronized是一个关键字,ReentrantLock是一个类
  2. sychronized会自动的加锁与释放锁,ReentrantLock需要程序员手动加锁与释放锁
  3. sychronized的底层是JVM层面的锁,ReentrantLock是API层面的锁
  4. sychronized是非公平锁,ReentrantLock可以选择公平锁或非公平锁
  5. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock 通过代码中int类型的state标识来标识锁的状态
  6. sychronized底层有一个锁升级的过程