java对象存储在内存中,共分为以下三个部分
1)、对象头
2)、实例数据
3)、对齐填充字节
java对象头有以下三部分组成:
1)、Mark Word
2)、Class Metadata Address(指向类的指针)
3)、Array Length(数组长度,只有数组对象才有)
JVM中对象头的方式有以下两种(以32位JVM为例):
1.1、普通对象:
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
1.2、数组对象:
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
Mark Word 主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄、对象锁、GC等有关的信息,当这个对象被synchronized关键字作为同步锁时,围绕这个锁的一系列操作都和Mark Word有关。Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit
2.1.1、Mark Word(32位)
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|-------------------------------------------------------|--------------------|
2.1.2、Mark Word(64位)
|------------------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | Normal |
|------------------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | Biased |
|------------------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | lock:2 | Lightweight Locked |
|------------------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | lock:2 | Heavyweight Locked |
|------------------------------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|------------------------------------------------------------------------------|--------------------|
32位和64位除了位数不同外,各个部分表示的含义是一样的,其中各部分的含义如下:
lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同,如下所示:
biased_lock | lock | 状态 |
---|---|---|
0 | 01 | 无锁 |
1 | 01 | 偏向锁 |
0 | 00 | 轻量级锁 |
0 | 10 | 重量级锁 |
0 | 11 | GC标记 |
biased_lock:对象是否启用偏向锁标记,默认启用偏向锁,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是 -XX:MaxTenuringThreshold
选项最大值为15的原因
identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法 System.identityHashCode()
计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中
thread:持有偏向锁的线程ID
epoch:偏向时间戳
ptr_to_lock_record:指向栈中锁记录的指针
ptr_to_heavyweight_monitor:指向管程Monitor的指针
下面我们着重介绍一下 synchronized 同步关键字锁的实现过程,下面以 Mark Word(32位)为例,Mark Word在不同的锁状态下存储的内容不同,如下所示:
锁状态 |
25bit |
4bit |
1bit |
2bit |
|
23bit |
2bit |
是否偏向锁 |
锁标志位 |
||
无锁 |
对象的HashCode |
分代年龄 |
0 |
01 |
|
偏向锁 |
线程ID |
Epoch |
分代年龄 |
1 |
01 |
轻量级锁 |
指向栈中锁记录的指针 |
00 |
|||
重量级锁 |
指向重量级锁的指针 |
10 |
|||
GC标记 |
空 |
11 |
JDK1.6以后的版本在处理同步锁时加入了锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁
JVM synchronized 锁通过以下流程来修改 Mark Word 的,整个锁的流程用下面一张图概括为:
2.1.3、默认开启偏向锁
1)、当对象没有被加锁时,即普通对象,则偏向锁状态为 1,锁标志位为 01,线程ID为0,如下所示:
2)、当对象被加锁并有一个线程A抢到了锁时,偏向锁状态位和锁标志位都不变,只是将前 23bit 记录抢到锁的线程id记录到线程ID中,对象进入偏向锁状态,如下所示:
当没有其他线程竞争该对象锁时,线程A释放锁后,对象会继续保持偏向锁状态,即 步骤 2 的状态,线程ID还是维持线程A的ID
3)、当线程A再次来获取锁时,JVM发现对象处于偏向锁状态,即 锁标志位是01,偏向锁标志也是 1,而且又是同一个线程,则只需要将获取锁的次数加1即可,无需重新获取锁,直接执行代码逻辑
4)、当线程B来获取锁时,JVM发现对象处于偏向锁状态,线程ID记录的不是线程B,则线程B 会通过CAS 自旋获取锁,如果 B 获取锁成功,则对象处于偏向锁状态,线程ID 修改为线程 B 的id,否则执行步骤5
5)、由于B没有获取到锁,说明锁竞争比较激烈,此时偏向锁会升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针,上述两个保存操作都是CAS操作,如果保存成功,则表示获取锁成功,锁标志位修改为 00,此时对象进入轻量级锁状态,如下所示:
6)、如果 B 获取锁失败,即表示锁升级为轻量级锁失败,此时锁会膨胀为重量级锁,锁的标志位修改为 10,此后没有获取到锁的线程全部会处于阻塞状态,不会自旋获取锁。JVM会单独创建一个 ObjectMonitor 对象监视器,Mark Word 指针指向该监视器,所有阻塞的线程会加入链表中排队等待获取 ObjectMonitor 对象锁(即 monitorenter、monitorexit 逻辑),如下所示:
一旦对象锁膨胀为重量级锁后,即使所有线程释放锁后,对象锁也不会恢复到无锁或者偏向锁状态了,此后所有线程的加锁操作都是重量级锁过程
2.1.4、禁用偏向锁(-XX:-UseBiasedLocking)
1)、当对象没有被加锁时,即普通对象,则偏向锁状态为 0,锁标志位为 01,对象的hashCode值,如下所示:
2)、当对象被加锁并有一个线程A抢到了锁时,JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针,上述两个保存操作都是CAS操作,如果保存成功,则表示获取锁成功,锁标志位修改为 00,此时对象进入轻量级锁状态
当没有其他线程竞争该对象锁时,线程A释放锁后,对象会重新恢复到无锁状态,即 步骤 1 的状态
3)、当线程A再次来获取锁时,JVM发现对象如果处于无锁状态时,则按照步骤2执行,否则(重入锁)只需要将获取锁的次数加1即可,无需重新获取锁,直接执行代码逻辑
4)、当线程B来获取锁时,JVM发现对象处于轻量级锁状态,JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针,上述两个保存操作都是CAS操作,如果保存成功,则表示获取锁成功,锁标志位修改为 00,此时对象进入轻量级锁状态,如下所示:
5)、如果 B 获取锁失败,此时锁会膨胀为重量级锁,锁的标志位修改为 10,此后没有获取到锁的线程全部会处于阻塞状态。JVM会单独创建一个 ObjectMonitor 对象监视器,Mark Word 指针指向该监视器,所有阻塞的线程会加入链表中排队等待获取 ObjectMonitor 对象锁(即 monitorenter、monitorexit 逻辑),如下所示:
一旦对象锁膨胀为重量级锁后,即使所有线程释放锁后,对象锁也不会恢复到无锁或者偏向锁状态了,此后所有线程的加锁操作都是重量级锁过程
2.1.5 java 输出对象信息
1)、maven 引入 jol-core jar 包,如下所示:
org.openjdk.jol
jol-core
0.13
2)、示例demo:
/**
* synchronized 测试
*
* -XX:-UseBiasedLocking 禁止偏向锁,默认偏向锁是开启的
*
* @author supu
* @since 2020-08-31 11:00
**/
public class SynchronizedDemo {
public static void main(String[] args) {
System.out.println(VM.current().details());
A a = new A();
a.setAge(10);
a.setName("zs");
ClassLayout c2 = ClassLayout.parseInstance(a);
System.out.println(c2.toPrintable());
new Thread(() -> {
synchronized (a){
System.out.println("*********** a 第一次被锁定后的对象头 markword ***********");
System.out.println(c2.toPrintable());
/*synchronized (a){
System.out.println("*********** a 第二次被锁定后的对象头 markword ***********");
System.out.println(c2.toPrintable());
}*/
try {
TimeUnit.MILLISECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
try {
TimeUnit.MILLISECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 当多个线程竞争锁时,偏向锁升级为轻量级锁还是重量级锁的条件时,后者获取锁的线程是否能在指定时间内自旋CAS获取到锁,
// 如果获取到锁了则升级为轻量级锁,否则升级为重量级锁
synchronized (a){
System.out.println("*********** a 第二次被锁定后的对象头 markword ***********");
System.out.println(c2.toPrintable());
}
System.out.println("************ a 释放锁定后的对象头 markword **********");
System.out.println(c2.toPrintable());
}
static class A {
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
A a = (A) o;
return age == a.age &&
Objects.equals(name, a.name);
}
@Override
public int hashCode() {
return Objects.hash(age, name);
}
}
}
类的指针用于存储对象的类型指针,该指针指向它的类元数据信息,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。
如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项 +UseCompressedOops
开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:
1)、每个Class的静态变量属性指针(即静态变量)
2)、每个对象的成员变量属性指针(即成员变量)
3)、普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。
如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启 +UseCompressedOops
选项,该区域长度也将由64位压缩至32位
对象的实例数据就是在java代码中能看到的属性和他们的值
因为JVM要求java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数,除此之外没有特别的功能
五、参考
https://wiki.openjdk.java.net/display/HotSpot/Synchronization
https://www.jianshu.com/p/3d38cba67f8b