目录
Java对象存储:(HOTSPOT虚拟机)
一个Java对象可以分为三部分存储在内存中,分别是:
- 对象头Object Header
- 实例数据Instance
- 对齐填充Padding
什么是对象头,具体包括什么
在HotSpot虚拟机中,对象头分为两部分(普通对象)
- 1.对象自身的运行时数据: 包括存储哈希码,GC分代年龄,锁状态标记,线程持有的锁,偏向锁ID,偏向时间戳等.这部分被叫做mark word,在32位和64位的虚拟机分别占32bits和64bits.
- 2.对象的类型指针: 即指向对象的类元数据的指针.虚拟机可以通过该指针判定对象实例属于那个类.
注:在Java对象中比较特殊的是数组,一个数组实例的对象头中必须包含数组的长度.JVM可以通过对象头中的数组长度数据来判定数组的大小,这是访问数组类型的元数据是无法得知的
JVM中对象头有两种:
1.普通对象
Object Header: 包括mark word和class pointer
2.数组对象
Object Header : 包括mark word,class pointer和array length
数组特有的length ,JVM需要通过数组对象头的length来判定数组的大小.
Mark Word
64位虚拟机
32位虚拟机
Mark Word分析 (64位):
-
对象的hashcode(identity hash code):
占31bit,根据内存地址生成的hash值. -
GC分代年龄:
age 占4bits,在GC过程中,如果新生代对象在Survivor区复制一次,年龄+1.当对象年龄达到设定的阈值时,对象将会进入老年代.或者有一半的的对象年龄相等,则大于该年龄的对象直接进入老年代.默认情况GC年龄阈值为15.因为age只占4位 ,所以最大值为1111= 15. -
锁标志位: 占2bit位,希望用尽可能少的二进制位表示尽可能多的信息,所以设置了锁的标记.该标记的值不同,整个mark word 表示的含义不同.
-
biased_lock(是否为偏向锁) : 对象是否为偏向锁,只占1个bit. 为1时表示对象启用偏向锁,为0表示对象没有偏向锁.
无锁和偏向锁的锁标志都是01 ,区分的方式是通过1bit的是否为偏向锁(biased_lock).偏向锁的这1bit等于1,无锁这一位则为0
-
锁的状态
-
new (未锁定状态)
在一个对象刚new出来的时候,没有线程竞争,就是一个普通对象,不需要加锁,此时biased_lock为1,锁的标志位为01 , 表示无锁可偏向 . ( 如果计算了对象的hashcode ,则会记录对象的hashcode,锁的标志位为01.biased_lock位置为0. 此时表示无锁不可偏向状态) -
偏向锁:biased_lock
如果已经计算了对象的hashcode ,则表示该锁不能偏向 .直接升级为轻量级锁.(对象的hashcode和偏向线程id只能存储一个)
1.当第一个线程A来获取资源的时候,这个时候只有线程A一个,没有其他线程来竞争,他会将biased_lock标志位置为1,锁标志为01, 表示已经偏向它的状态.线程ID也会记录A的id.
2.当A线程再次获取该资源的时候,JVM发现mark word里面的线程id是A的id,锁的标志位是01,biased_lock是1,表示A已经获得该偏向锁. -
轻量级锁(自旋,自旋锁,look-free,CAS轻量级锁):
当线程B尝试获取该锁时(此时有了锁的竞争),JVM发现此时锁处于偏向状态,mark word的线程id记录的是A,此时线程B会尝试通过CAS的方式(在用户空间)获取锁.两个线程都将对象的hashcode复制到自己新建的用于存储锁的记录空间LockRecord,通过CAS的操作,将对象的mark word的内容修改为自己新建的记录空间的地址来竞争mark word.成功则获取资源,失败则继续CAS操作.
自旋的线程在自旋过程中,成功获取资源,整个状态仍然处于轻量级锁的状态. -
重量级锁:
竞争加剧 在jdk6: 自旋锁自旋次数超过10,或者等待线程超过CPU核数的二分之一,升级为重量级锁
jdk6以后:jdk自适应自旋,来判断什么时候升级自旋的线程将被阻塞,需经过os,提供一个等待队列和一个竞争队列.等待操作系统的调度
-
-
当前线程指针: 存放获取偏向锁的线程id,占54bit
-
Epoch: 偏向锁的时间戳,占2bit
-
ptr_to_lock_record: 轻量级锁状态下,指向栈中锁记录的指针。
-
ptr_to_heavyweight_monitor: 重量级锁状态下,指向对象监视器Monitor的指针。
Class Pointer (类型指针)
这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例.
-XX:+UseCompressedClassPointers 为4字节(默认开启) 不开启为8字节 (对象属于哪个Class)
array length(数组特有)
如果对象是一个数组,那么对象头还要有额外的空间存储数组的长度,JVM可以通过对象头中的数组长度数据来判定数组的大小,这是访问数组类型的元数据是无法得知的. 长度为4个字节
实例数据(Instance)
对象头是对象的额外开销,只有实例数据才是一个对象实例存储的有效信息,也是在程序代码中所指定的各种类型的字段内容.这部分内容同时记录了子类从父类继承所得的各类型数据.
-XX:+UseCompressedOops 为4字节(默认开启) 不开启为8字节 Oops Ordinary Object Pointers(成员变量的引用 比如Object o)
对其填充(padding)
对齐填充在对象数据中并不是必然的,只是起着占位符的作用,没有特别含义.HotSpot要求对象起始地址必须是8字节的整数倍.对象头的大小刚好符合要求,因此当实例数据没有对齐时,就由对齐填充来对齐数据.
如何获取的元数据
虚拟机在加载类的时候会将类的信息,常量,静态变量和即时编译器编译后的代码等数据存储在方法区(Method Area).类的元数据,即类的数据描述也被存在方法区.我们知道对象头中会存有对象的类型指针,通过类型指针可以获取类的元数据.因此,对象的类型指针其实指向的是方法区的某个存有类的信息地址
有两种对象定位的方法:(间接和直接)
通过句柄访问对象(间接)
对象的类型指针被放在句柄池中;
通过实例数据指针去堆中找对象,同时通过类型数据指针去方法区找对象的类型
通过Reference指针直接访问对象(直接)
对象的类型指针被存放在对象本身的数据中
先通过实例数据指针去堆中找到对象,再通过类型数据指针去方法区找对象的类型.
对象的基本存储方式:
- 对象的引用:存储在栈中
- 对象的实例数据:存储在堆中
- 对象的元数据:存储在方法区
补充
在jdk8中为什么大于4秒钟的时候才会有偏向锁状态?
其实这是因为虚拟机在启动的时候对于偏向锁有延迟。
因为在项目启动的时候,有大量的同步块(synchronized),多个线程访问的时候,需要消除偏向锁。会很麻烦,反而会降低效率。
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 修改上面的jvm参数,可以开启jvm偏向锁延迟配置,延迟为0
-XX:-UseBiasedLocking关闭偏向锁
锁状态转换:
代码示例
public class ObjectTest {
static ObjectTest a = new ObjectTest(); // 1
public static void main(String[] args) {
System.out.println("new 后"+ClassLayout.parseInstance(a).toPrintable());
a.hashCode(); // 2
System.out.println("计算hashcode后"+ClassLayout.parseInstance(a).toPrintable());
synchronized (a) { // 3
System.out.println("偏向后"+ClassLayout.parseInstance(a).toPrintable());
}
new Thread("t1"){ // 4
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock();
}
}.start();
new Thread("t2"){ //5
@Override
public void run() {
lock();
}
}.start();
}
public static void lock(){
synchronized (a){
System.out.println(Thread.currentThread().getName()+ClassLayout.parseInstance(a).toPrintable());
}
}
}
添加依赖
<dependencies>
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.11</version>
</dependency>
</dependencies>
通过ClassLayout.parseInstance(Instance).toPrintable()方法查看对象布局.
new一个对象, 一开始为无锁可偏向状态.有线程操作 ,则偏向该线程,设置偏向线程id为该线程id.
如果new一个对象之后计算了该对象的hashcode,则锁状态为 无锁不可偏向状态 ,会存储hashcode值 , 有线程来会直接升级为轻量级锁.
new一个对象,先有一个线程, 则偏向线程id设置为该线程id ,又来一个线程,有了锁的竞争 ,则升级为轻量级锁.