Java对象头(64位虚拟机)
- 整个对象头一共有128位,Mark Word有64位,Klass Word有64位,但是Klass Word因为指针压缩的原因被压缩为32位,使用对象头一共有96位,而且现在我们更需要关注的是前64位。至于Klass Word指向了方法区的模板类,字节码。
- 对象头还不是一成不变的,就表格可以看出,对象的状态会改变对象头的数值,这里我们分为5个状态,分别是无锁(001)、偏向锁(101)、轻量锁(00)、重量锁(10)和被gc标记(11)的对象。
对象头占据内存
- 例如:int是4个字节,Integer对象的Mark Word8字节,Klass Word8个字节(指针压缩后4字节),存储占4字节,存储一个数需要占8(Mark Word)+4(Klass Word)+4(存储占4字节)=16字节
验证new Integer(1)占据内存大小
org.openjdk.jol
jol-core
0.9
@Slf4j(topic = "ants.TestIntegerSize")
public class TestIntegerSize {
public static void main(String[] args) {
Integer i = new Integer(1);
log.debug(ClassLayout.parseInstance(i).toPrintable());
}
}
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
2020-04-12 17:45:45.202 [main] DEBUG ants.TestIntegerSize 16 - java.lang.Integer object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) ae 22 00 f8 (10101110 00100010 00000000 11111000) (-134208850)
12 4 int Integer.value 1
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
- 由上面控制台结果分析,new Integer(1)会占据16字节,而我们知道int i=1只会占四个字节。
Monitor(锁) 监视器|管程
-
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级锁)之后,该对象头的Mark Word就被设置指向Monitor对象指针。
- synchronized(obj)使用对象锁(obj)和Monitor对象一一对应
- 刚开始Monitor中Owner为null
- 当Thread-2执行synchronized就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
- Thread在上锁过程中,如果Thread-3,thread-4,thread-5也来执行synchronized(obj),就会进入EntryList中进阻塞(BLOCKED)状态
- Thread-2执行完同步代码块内容,然后唤醒EntryList中等待的线程来竞争锁,竞争时是非公平的(synchronized没有提供公平锁,非公平是指如果Thread-2释放锁的时候来了Thread-6,那么Thread-3和Thread-6将同时有机会获得obj对象)
- 图中Thread-0,Therad-1是之前获得过锁,但是条件不满足进入WAITING状态的线程(跟wait-notify相关)
注意
- synchronized必须是关联统一个对象时,才会出现上面结果
- 不加synchronized不会关联monitor监视器,不会出现上面结果
synchronized原理
- dup:将lock复制一份
- Exception table:检测一定范围,如果范围内出现异常做处理
- 6-16-19-any,表示如果在6-6行出现异常,java会在19行开始执行,同样保存lock引用,将对象头重置,唤醒EntryList,让其他线程竞争锁,最后将处理不了的异常抛出去
- 在字节码中可以看出,即使synchronized中出现异常,synchronized也会释放锁
不同锁
- Monitor
- 轻量级锁
- 偏向锁
- 批量重偏向(一个类的偏向锁撤销达到20阈值)
- 一个类的偏向锁撤销达到40阈值,对象升级为轻量级锁
轻量级锁
- 使用场景:如果一个对象虽然有多线程访问,单多线程访问的时间是错开的(没有线程竞争),name可以用轻量级锁来优化。
- 语法:轻量级锁对使用这透明,仍然使用synchronized
轻量级锁加锁、解锁流程
public class LightLock {
static final Object object = new Object();
public static void method1(){
synchronized (object){
// 同步代码块1
method2();
}
}
public static void method2(){
synchronized (object){
// 同步代码块2
}
}
}
-
创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录,内部可以存储锁定对象的Mark Word
-
让锁记录中Object Reference指向锁对象,并尝试使用cas替换Object的Mark Word的值存入锁记录
-
如果替换成功,对象头中存储了锁记录地址和状态00(代表轻量级锁),表示该线程给对象加锁,--(object对象头中Mark Word发生变化,原先01无锁状态变成00轻量级锁状态,另外分代年龄等信息变为锁记录地址),--(锁记录里面将记录hash码、分代年龄等信息)(--锁记录和Mark Word信息互换)
-
如果cas失败,分为两种情况
4.1 如果是其他线程已经持有了该Object的轻量级锁,这时表名有竞争,进入锁膨胀过程
4.2 如果是自己执行了synchronized表示锁重入,那么再添加一条Lock Record作为重入的计数
-
当退出synchronized代码块(解锁时),如果取值为null的锁记录,这时重置锁记录,表示重入计数减一
- 当退出synchronized代码块(解锁时),如果取值不为null的锁记录,这时cas将Mark Word的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进程锁膨胀已经升级为重量级锁,进入重量级锁的解锁流程
重量级锁加锁、解锁流程 - 如果在尝试加轻量级锁的过程中,cas操作无法成功,这时一种情况是其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁升级为重量级锁
public class WeightLock {
static final Object object = new Object();
public static void method1(){
synchronized (object){
// 同步代码块1
}
}
}
-
当Thread-1进入轻量级锁加锁时,Thread-0已经对该对象加了轻量级锁
-
这时Thread-1加锁失败,进入锁膨胀流程
2.1 为Object对象申请Monitor锁,让Object指向重量级锁地址
2.2 然后线程Thread-1进入Monitor的EntryList阻塞(BLOCKED)状态
- 当Thread-0退出同步代码块解锁时,使用cas将Mark Word的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中的BLOCKED线程
重量级锁竞争时自旋优化
- 重量级锁竞争时候,可以通过自旋来进行优化,如果当前线程自旋成功(即这时保持锁的新城已经退出了同步代码块。释放了锁),这时当前线程就可以避免阻塞
- 以牺牲CPU,多核CPU情况才能发挥优势
- Java6之后自旋是自适应的,比如对象刚刚一次自旋成功过,就认为自旋成功可能性大,就多自旋几次;反之少自旋甚至不自旋。
- Java7之后不能控制是否开启自旋功能
- 自旋失败后,进入EntryList阻塞(BLOCKED)状态
偏向锁优化 - 轻量级锁不需要Monitor对象,使用线程栈帧中的锁记录充当轻量级锁
-
轻量级锁在发生锁重入的时候,是需要cas操作,插入一条值为null的锁记录
-
Java6引入偏向锁:只有第一次使用cas将线程id设置为对象头的Mark Word头,之后发现这个线程di是自己的就表示没有竞争,不用重新cas操作。以后只要不发生竞争,这个对象就归该线程所有
- 当创建一个对象时:
- 如果开启了偏向锁(默认开启),那么对象创建后,markword值最后三位是101,这是它的thread、epoch、age都为0
- 偏向锁默认是延迟的,不会再程序启动时立即生效,如果想避免延迟,可以加-XX:BiasedLockingStartupDelay=0来禁用延迟
- 如果没有开启偏向锁,那么对象创建后,markword值最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值
验证偏向锁
/**
* @author magw
* @version 1.0
* @date 2020/4/12 下午3:40
* @description: No Description
* 偏向锁+对象内存大小
1).大端存储:大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放。
2).小端存储:小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。
*/
@Slf4j(topic = "ants.TestBiased")
public class TestBiased {
public static void main(String[] args) {
Object i = new Object();
System.out.println(Integer.toHexString(i.hashCode()));
System.out.println(ClassLayout.parseInstance(i).toPrintable());
}
}
2280cdac
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 ac cd 80 (00000001 10101100 11001101 10000000) (-2134004735)
4 4 (object header) 22 00 00 00 (00100010 00000000 00000000 00000000) (34)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
-
从控制台看,hacode(2280cdac)和对象头(accd8022)hashcode(对象头25-56)内容不符合,这是因为我电脑是小端存储,即数据低地址存放数据地位,应该反来读取,即(2280cdac),这时对象头和hashcode对应起来
- 但是我们发现对象头最低3位是001,代表是正常状态,并不是偏向锁状态?这时因为偏向锁默认是延迟的,可以采取两种方式进行来显示出对象头中的偏向锁状态
2.1 程序启动时,在对象new之前睡眠>=4秒,例如Thread.sleep(5000);
2.2 设置vm启动参数,-XX:BiasedLockingStartupDelay=0
2.3 关闭偏向锁:-XX:-UseBiasedLocking - 线程在修改完对象头中信息后,如果再次访问,对象头中的线程id不会变
public class TestBiased {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
Object i = new Object();
System.out.println(ClassLayout.parseInstance(i).toPrintable());
}
}
撤销偏向锁
- 如果调用锁对象的hashcode()方法,会撤销对象偏向锁,如果遇到同步,直接升级为轻量级锁。调用完hashcode后,线程对象头没有地址存放hashcode值。轻量级锁调用hashcode后会将hashcode保存到锁记录里,重量级锁调用hashcode,会将hashcode保存到moitor里。
- 其他对象使用拥有偏向锁对象(两个线程交叉执行),偏向锁会升级轻量级锁,当代码执行后,对象的偏向锁会撤销
- 调用wait/notify后,也会撤销偏向锁
@Slf4j(topic = "ants.CannelBiasedMethod")
public class CannelBiasedMethod {
public static void main(String[] args) {
Object o = new Object();
new Thread("t1") {
@Override
public void run() {
log.debug(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
log.debug(ClassLayout.parseInstance(o).toPrintable());
}
log.debug(ClassLayout.parseInstance(o).toPrintable());
synchronized (CannelBiasedMethod.class){
CannelBiasedMethod.class.notify();
}
}
}.start();
new Thread("t2") {
@SneakyThrows
@Override
public void run() {
synchronized (CannelBiasedMethod.class){
CannelBiasedMethod.class.wait();
}
log.debug(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
log.debug(ClassLayout.parseInstance(o).toPrintable());
}
log.debug(ClassLayout.parseInstance(o).toPrintable());
}
}.start();
}
}
批量重偏向
- 如果对象虽然被多个线程访问,但是没有竞争,这是偏向了线程t1的对象仍有机会重新偏向t2,重偏向会重置对象的Thread ID
- 当撤销偏向锁阈值超过20次后,jvm会在给这些对象加锁时重新偏向至加锁线程
- 线程1给对象线程id修改,t2线程获得对象的线程id不变,t2在执行同步代码块时,锁升级为轻量级锁,同步代码块执行完,对象变成无锁状态
- 在达到20次后,后面的对象中的线程id就会被修改,不会再偏向锁-轻量级锁-无锁状态转化
//查看t2线程执行的执行的第19和20次,发现线程id变化,在t2后面的线程id,不在会变化
@Slf4j(topic = "ants.BatchBiased")
public class BatchBiased {
static Thread t1,t2;
public static void main(String[] args) {
int count = 39;
Vector
批量撤销
- 当撤销偏向锁超过40次后,类再新建的都是不可偏向的对象。
锁消除 - JVM会在JIT字节码进一步优化,会在字节码层面优化。会优化掉部分代码。
- xx:-EliminateLocks,如果能确认某个加锁的对象不会逃逸出局部作用域,就可以进行锁删除。.