在 JVM 中,使用 OOP-KLASS 模型表示 JAVA 对象
一、对象创建
-
jvm 在加载 class 时,会创建 instanceKlass ,表示其元数据,包括常量池、字段、方法等,存放在方法区。instanceKlass 是 jvm 中的数据结构。
-
在new一个对象时,jvm 创建 instanceOopDesc,来表示这个对象,存放在堆区;其引用,存放在栈区;它用来表示对象的实例信息。instanceOopDesc 对应 java 中的对象实例。
-
Hospot 并不把 instanceKlass 暴露给 java,而会另外创建对应的 instanceOopDesc 来表示 java.lang.Class 对象,并将后者称为前者的"Java镜像",klass 持有指向 oop 引用 (
_java_mirror
便是该 instanceOopDesc 对 Class 对象的引用)。 -
new 操作返回的 instanceOopDesc 类型指针(也叫元数据指针)指向 instanceKlass,instanceKlass 指向了对应的类型 Class 实例的 instanceOopDesc。
二、instanceOopDesc
JDK为1.8,因此是默认开启了指针压缩
下面通过修改VM参数,来开启或关闭指针压缩:
-XX:+UseCompressedOops // 开启指针压缩
-XX:-UseCompressedOops // 关闭指针压缩
对象保存在内存时,由三部分组成:
- 对象头
- Mark Word (标记字段)
(标记字段用以存储 Java 虚拟机有关该对象的运行数据,如哈希码、GC 信息以及锁信息)
在32位 JVM 中的长度是 32 bit,在64位 JVM 中长度是 64 bit (8字节) - Klass Pointer (类型指针 指向类的指针)
开启压缩是4字节,没有开启压缩是8字节 - 数组长度 (只有数组对象才有) (4字节)
- Mark Word (标记字段)
- 实例数据
存储对象的所有成员变量,static 成员变量不包括在内 - 对齐填充字节
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在不同的状态下存储的信息:
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空转。
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
(锁)。
4.自旋锁
自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量
六、sychronized和ReentrantLock的区别
- sychronized是一个关键字,ReentrantLock是一个类
- sychronized会自动的加锁与释放锁,ReentrantLock需要程序员手动加锁与释放锁
- sychronized的底层是JVM层面的锁,ReentrantLock是API层面的锁
- sychronized是非公平锁,ReentrantLock可以选择公平锁或非公平锁
- sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock 通过代码中int类型的state标识来标识锁的状态
- sychronized底层有一个锁升级的过程