前言
上篇文章末尾抛出了一个疑问,大家了解了JMM模型,那Java对象模型又是什么?这个模型有什么用?在什么场景中使用?本文从Java对象模型作为切入点,来看看Java对象模型是什么东西,它是用来解决什么问题的。
从一个case看Java对象内存模型
创建一个简单的订单类,我们通过订单类来看看内存中的存储格式是什么样子,订单类中包含订单号、付款人和支付金额三个属性,代码如下:
public class Order {
// 订单号
private long orderNo;
// 支付用户
private long payUser;
// 订单金额
private long amount;
public long getOrderNo() {
return orderNo;
}
public void setOrderNo(long orderNo) {
this.orderNo = orderNo;
}
public long getPayUser() {
return payUser;
}
public void setPayUser(long payUser) {
this.payUser = payUser;
}
public long getAmount() {
return amount;
}
public void setAmount(long amount) {
this.amount = amount;
}
}
通过Order
类创建一个对应的实例对象,实例对象在JVM内部的理论存储模型如下图所示,从图中可以看出,一个Java对象由对象头、实例数据和对齐填充三部分构成,下面我们通过jol
工具实践验证一下Order
对象在内存中的存储结构。
引入Maven依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
配置JVM参数,JVM默认是开启指针压缩的,该参数是用于关闭指针压缩的
-XX:-UseCompressedOops
执行如下代码
public static void main(String[] args) {
Order order = new Order();
String printable = ClassLayout.parseInstance(order).toPrintable();
System.out.println(printable);
}
运行结果如下图所示,对象头主要由 Mark Word
8个字节和 Klass指针
8个字节组成;实例数据则根据内部属性类型分配相应的空间;看到这里大家会有个疑问,不是还有对齐填充吗?怎么这里没有看到,因为现在情况刚好是8的整数倍,所以不需要额外空间进行填充,自然在这里不会体现对齐填充信息。
小结 通过这个case可以看出,Java对象模型基本上由对象头(Mark Word、Klass等)、实例数据和对齐填充构成,下面我们来分别深入看看各个部分的内部构成和使用场景。
Mark Word
在不同锁类型下,Mark Word
的存储信息是动态变化的,下面我们来分别探讨下不同模式下 Mark Word
存储的关键信息
无锁
分代年龄(4bit):用于标识对象的GC信息,为JVM进行GC提供依据
HashCode(31bit):表示当前对象的hash值
偏向锁
当前线程指针(54bit):标识持有当前对象锁的线程指针,即哪个线程可以操作当前对象,通过该线程指针即可找到对应的线程,然后通过该线程来操作当前对象完成业务逻辑的处理
分代年龄(4bit):用于标识对象的GC信息,为JVM进行GC提供依据
在JDK15中,偏向锁被废弃了,官方的解释是偏向锁为同步系统引入了许多复杂的代码,并且对HotSpot的其他组件产生了影响。还有一种解释是计算机发展到了现在,生产环境本身已处于大量的高并发场景下,使用偏向锁会使得系统带来更大的性能开销。官网地址:** https://openjdk.org/jeps/374 **
轻量级锁
指向线程栈中 lock record指针(62bit),表示在多个线程开始竞争,此时的竞争程度还不太激烈,该指针指向了获取锁成功的线程。
重量级锁
monitor监视器的地址(62bit):mark word 指向一个monitor监视器的地址,monitor其实就是大名鼎鼎的synchroized在升级为重量级锁后的内部实现,其底层通过操作系统的
Mutex Lock
实现的,由于使用Mutex Lock需要将当前线程挂起(线程状态变为阻塞)并从用户态切换到内核态来执行,这种切换的代价非常高,这也就解释了为什么叫重量级锁(关于锁升级,monitor实现,这个我们在下篇讲synchroized的时候在详细展开)。
Klass 指针
一个Klass对象代表一个类的元数据,大白话可以理解为是创建实例对象的一个模版,那这个模版在JVM中是如何体现的呢?我们知道自己new出来的Class对象,它其实是klass包装了一层然后暴露给Java层使用,而klass是提供一个与java类对等的c++类型描述,是用于JVM内部使用的。在对象头中,通过Klass指针使得实例对象与其对应的类元数据信息关联起来,以便通过这个指针,找到该实例对象对应的元数据信息。所有的klass信息是存放在元数据信息区中。
实例数据
实例数据就是通过new创建出来的对象的具体属性值,比如文章开头Order类对应的实例,其中的orderNo、payUserId、orderAmount属性对应的具体值就是实例数据。
对齐填充
对齐填充并非必须,仅仅为了起到占位符的作用。由于HotSpot虚拟机的内存管理系统要求对象起始地址必须是8字节的整数倍,所以对象头正好是8字节的倍数。因此当对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
后续
本文重点介绍了Java对象模型的组成部分,通过对象头的中的 ** mark word ** 中存储的元数据信息,成为任何对象都可以作为锁的基础,这也是synchroized关键字实现的基础;通过对象头中的 Klass 指针 将Java层和JVM底层实现了桥接。
下篇文章我们通过分析synchroized再来看看基于Java对象头实现的锁是如何工作的。
做一个有深度的技术人