多线程与高并发(4)——对象头和锁升级

本文主要总结对象头以及锁升级的过程。

一、对象布局

对象实例在堆内存中被分为三部分:对象头、实例数据、对齐填充。
对象头又包括:markWord,类型指针和、数组长度(可选,数组类型时才有)。
多线程与高并发(4)——对象头和锁升级_第1张图片
1、markword这部分其实就是加锁的核心,同时还包含的对象的一些生命信息,例如是否GC、进过了几次Young GC还存活。记录了对象和锁有关的信息,在64位的JVM中,Mark Word为64 bit。后面详细列出来。
2、class pointer记录了指向对象的class文件指针。
3、instance data记录了对象里面的实际变量数据。
4、padding是会将对象所占的字节对齐到8的倍数。对象在64位服务器版本中,规定对象内存必须要能被8字节整除,如果不能整除,那么就靠对齐来补。举个例子:new出了一个对象,内存只占用18字节,但是规定要能被8整除,所以padding=6。

二、对象头(markword)

锁状态的记录主要存储在markword中,markword的结构如下以64位为例,markWord的结构通过以下代码来看:

import org.openjdk.jol.info.ClassLayout;

public class synDemo {
    private static Object  o;
    public static void main(String[] args) {
        o = new Object();
        synchronized (o){
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

输出结果如下所示,我们先看未加锁时的执行结果:
多线程与高并发(4)——对象头和锁升级_第2张图片
我们从上图可以看出:
(1)对象头(Object Header)包含了12个字节分为3行,其中前2行其实就是markword,第三行就是class指针;
(2)实例数据为16个字节;
(3)对齐为4个字节,这样总数就为32是8的倍数。
加锁后的执行结果如下:
多线程与高并发(4)——对象头和锁升级_第3张图片
(4)加锁前后输出从001变成了000。
Markword用处:8字节(64bit)的头记录一些信息,锁就是修改了markword的内容8字节(64bit)的头记录一些信息。从001无锁状态,变成了00轻量级锁状态。
Markword结构如下:
多线程与高并发(4)——对象头和锁升级_第4张图片
拓展:什么样的对象会进入老年代?很多场景例如对象太大了可以直接进入,但是这里想探讨的是为什么从Young GC的对象最多经历15次Young GC还存活就会进入Old区(年龄是可以调的,默认是15)。上图中hotspots的markword的图中,用了4个bit去表示分带年龄,那么能表示的最大范围就是0-15。所以这也就是为什么设置新生代的年龄不能超过15,工作中可以通过-XX:MaxTenuringThreshold去调整,但是一般我们不会动。
参考:https://baijiahao.baidu.com/s?id=1704054204411782859&wfr=spider&for=pc

三、锁升级

锁升级我不想用大量的文字描述,直接看下图:
多线程与高并发(4)——对象头和锁升级_第5张图片
轻量级锁又是自旋锁,线程数少的情况下,用自旋锁;线程数多,操作时间长,则是转向重量级锁。锁升级的过程是不可逆的。
偏向的意义在于:第一个线程拿到锁,将自己的线程信息标记在锁上,下次进来就不需要在拿去拿锁验证了。如果超过1个线程去抢锁,那么偏向锁就会撤销,升级为轻量级锁,偏向锁并不算一把真正的锁,因为只有一个线程去访问共享资源的时候才会有偏向锁这个情况。
扩展:都说syn为重量级锁,那么到底重在哪里?
JVM偷懒把任何跟线程有关的操作全部交给操作系统去做,例如调度锁的同步直接交给操作系统去执行,而在操作系统中要执行先要入队,另外操作系统启动一个线程时需要消耗很多资源,消耗资源比较重,重就重在这里。
关于锁升级,可以看一篇有味道的文章:没错,我就是厕所所长!

你可能感兴趣的:(java,多线程,java)