Android开发

Java 对象头分析与使用(Synchronized相关)

2021-01-08  本文已影响0人  小鱼人爱编程

前言

线程并发系列文章:

Java 线程基础
Java “优雅”地中断线程
Java 线程状态
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有误
Java Unsafe/CAS/LockSupport 应用与原理
Java 并发"锁"的本质(一步步实现锁)
Java Synchronized实现互斥之应用与源码初探
Java 对象头分析与使用(Synchronized相关)

从上篇文章我们了解到:synchronized修饰代码块/修饰方法,最终都是在对象头上做文章,因此对象头是深入理解synchronized 各种锁变化的基础。接下来就来深入分析对象头在synchronized里的作用。
通过本篇文章,你将了解到:

1、对象在内存的构成
2、对象头的构成
3、对象头源码实现
4、调试查看对象头

1、对象在内存的构成

先看一个简单的类:

    class Student {
        int age;
        String name;
    }
    
    //实例化对象
    Student student = new Student();

我们知道,new 出来的对象放在堆里,而对象在堆里的结构如下:


image.png

分为三个部分:对象头、实例数据(age/name)、填充字节。

2、对象头的构成

对象头的划分

而对象头各区域如下:


image.png

只有数组对象才会有数组长度部分,接下来以普通对象为例说明。

Klass Word 指向对象所属类的元数据。

对象头的大小

以64位机器为例,对象头大小如下:


image.png

可以看出,普通对象的对象头大小为:128bits,Mark Word、Klass Word分别占据64bits。

Mark Word 构成

32位机器和64位机器有差别,以64位为例,将Mark Word 各个区域构成整理如下:


image.png

如上图所示,Mark Word 可以表示五种状态,同一时刻只能表示一种状态。如何确定Mark Word处于何种状态呢?
Mark Word 内容区域里不同的bit(位)存储不一样的信息,可以看到五种状态有一个共同的信息:lock。
lock 占2bits,可以表示四种状态:


image.png

lock可以表示四种状态,而Mark Word有五种状态,无锁和偏向锁lock取值是相同的,又如何来区分两者呢?可以看到两者有共同的信息位:biased_lock。
biased_lock 占1bit,可以区分两种状态:

1------>表示是偏向锁
0------>表示不是偏向锁

因此结合lock与biased_lock(共3个bit) 可以表示五种状态:


image.png

1、Mark Word 结构并不像常见的Java 对象拥有不同的成员变量,而是通过细化到bit来表示具体的值。
2、得益于第一点设计,Mark Word 可以在有限的空间内灵活的表示五种状态,节约了内存。

3、对象头源码实现

Mark Word 定义

弄清楚了Mark Word构成,来看看如何通过代码来表示状态并进行状态切换。
之前提到过,本系列并发文章源码基于jdk1.8,源码网址:
http://hg.openjdk.java.net/

查找到markOop.hpp文件:

image.png
markOopDesc提供了value()函数,该函数里返回了自身(指向该对象的指针),并强转为uintptr_t类型。
先看看uintptr_t:
image.png
64位的机器,uintprt_t表示8字节的无符号整形。
再看看markOopDesc的父类oopDesc:
在oop.hpp文件里:
image.png
该类里包含了:Mark Word和Klass Word(联合体),重点来看看markOop类型:
在oopsHierarchy.hpp文件里。
image.png
可以看出markOop其实就是markOopDesc 指针,就是说markOopDesc里的value()函数最终返回的就是markOop,也就是64bits的Mark Word(无符号整形)。

Mark Word 状态判断

既然拿到了Mark Word的内存值(64bits无符号整形),接下来就对该值做文章,比如如何判断该Mark Word是否处在无锁状态呢?
继续回到markOopDesc类,该类里提供了很多函数,以判断是否是无锁状态为例:


image.png

再来看看mask_bits,它是个内联函数:


image.png
可以看出,实际上就是将两个参数做"按位与"运算。
再回过头来看看mark_bits的参数,第一个参数就是value()返回的markOop,第二个参数隐藏比较深就不贴图了,此处直接说结论:biased_lock_mask_in_place = 0x111(7),而unlocked_value定义如下:
image.png

可以看到定义的枚举值和我们之前提到的Mark Word五种状态值一致。
最后判断是否是无锁状态简化如下:

markOop & 0x111(7) == 1 表达式为真即表示Mark Word处在无锁状态
实际上就是取出Mark Word对应的位进行判断

其它函数与上述函数类似。

4、调试查看对象头

JOL简单使用

源码是枯燥的,大多时候仅仅是帮助我们理解其原理。有时候并不需要了解其细节,只想知道结果。那么有没有方法知道当前对象头的值呢?如此就可以通过值判断属于哪种状态。
JOL(Java Object Layout) Java 对象布局,通过这个工具可以查看对象的信息:如对象头、实例内容、填充数据等。
在Android Studio里引用该工具:

1、在 https://repo.maven.apache.org/maven2/org/openjdk/jol/jol-cli/0.9/ 下载jol-cli-0.9-full.jar
2、在Android Studio里引用该jar
3、如果是Java环境的话,通过Maven引用

导入jol包后,来看看简单的使用过程:

public class TestDemo {
    public static void main(String args[]) {
        Object object = new Object();
        //打印虚拟机的信息
        System.out.println(VM.current().details());
        //打印对象大小
        System.out.println(ClassLayout.parseInstance(object).instanceSize());
        //打印对象头大小
        System.out.println(ClassLayout.parseInstance(object).headerSize());
        //打印对象信息
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
}

结果如下:


image.png

声明的object对象为空对象,因此对象里没有实例数据。
你也许发现了Klass Word 为32bits,说好的占用64bits呢?原因是Java VM默认开启了指针压缩。
关闭指针压缩:


image.png

Android Studio->Edit Configurations 编辑VM参数:

-XX:-UseCompressedOops

再运行结果如下:


image.png

可以看出Klass Word占用了8字节,并且因为本身已经对齐了,所以不需要填充对齐数据。

Mark Word状态查询

无锁

说到这了还是没提到怎么看锁状态,接下来看看。
上面的例子里object没有上锁,因此应该是无锁状态,重点是找Mark Word对应的位,上面提到过3bits确定Mark Word状态:


image.png

从这三3bits看,取值001,对应上述的表格可知为无锁状态。

轻量级锁

public class TestDemo {
    public static void main(String args[]) {
        Object object = new Object();
        //打印对象信息
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
        synchronized (object) {
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
}

结果如下:


image.png

上锁前后都是无锁状态,上了锁后是轻量级锁

偏向锁

说好的无锁->偏向锁->轻量级锁的演变过程呢,怎么直接就到了轻量级锁状态?
JVM 启动的时候没有立即开启偏向锁,而是延迟开启。原因猜测是刚开始竞争很激烈,偏向锁撤销会增加系统负担。
延迟时间是4s,在globals.hpp里可以找到:


image.png

既然知道了原因,那么在代码里延迟对象的创建。

public class TestDemo {
    public static void main(String args[]) {
        try {
            Thread.sleep(4500);
        } catch (Exception e) {

        }
        Object object = new Object();
        //打印对象信息
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
        synchronized (object) {
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
}

注意:此处对象创建需要放在延迟生效的后面,因为偏向锁启用后对已生成的对象没有影响。
结果如下:

image.png
可以看出,偏向锁一旦开启了,默认就是偏向锁。
当然如果不想每次都等几秒钟才出结果,可以设置VM参数,添加如下参数:

-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

用以禁用偏向锁延迟生效。

重量级锁

public class TestDemo {
    static Object object = new Object();
    public static void main(String args[]) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("before get lock in Thread1");
                System.out.println(ClassLayout.parseInstance(object).toPrintable());
                synchronized (object) {
                    System.out.println("after get lock in Thread1");
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            System.out.println("before get lock in Thread2");
                            System.out.println(ClassLayout.parseInstance(object).toPrintable());
                            synchronized (object) {
                                System.out.println("after get lock in Thread2");
                                System.out.println(ClassLayout.parseInstance(object).toPrintable());
                            }

                        }
                    }, "t2").start();

                    sleep(5000);
                }
            }
        }, "t1").start();
    }
}

以上开启了两个线程t1、t2,在它们获取锁前后打印对象。t1先执行,然后开启t2,t1睡眠5s。
分几个步骤分析:
t1未获取锁之前:

image.png

t1获取锁之后:

image.png

t2获取锁之前:

image.png

t2获取锁之后:

image.png
t2尝试获取锁时发现锁被其它线程占用(t1),尝试几次还是无法获取锁,就由轻量级锁膨胀为重量级锁,挂起自己。

至此,Java 对象头简单分析完毕。
无锁、偏向锁、轻量级锁、重量级锁源码下篇分析。

参考:
jdk1.8
https://www.cnblogs.com/lusaisai/p/12748869.html
https://cloud.tencent.com/developer/article/1658707

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android

1、Android各种Context的前世今生
2、Android DecorView 一窥全貌(上)
3、Android DecorView 一窥全貌(下)
4、Window/WindowManager 不可不知之事
5、View Measure/Layout/Draw 真明白了
6、Android事件分发全套服务
7、Android invalidate/postInvalidate/requestLayout 彻底厘清
8、Android Window 如何确定大小/onMeasure()多次执行原因
9、Android事件驱动Handler-Message-Looper解析
10、Android 键盘一招搞定
11、Android 各种坐标彻底明了
12、Android Activity/Window/View 的background
13、Android IPC 之Service 还可以这么理解
14、Android IPC 之Binder基础
15、Android IPC 之Binder应用
16、Android IPC 之AIDL应用(上)
17、Android IPC 之AIDL应用(下)
18、Android IPC 之Messenger 原理及应用
19、Android IPC 之获取服务(IBinder)
20、Android 存储基础
21、Android 10、11 存储完全适配(上)
22、Android 10、11 存储完全适配(下)
23、Java 并发系列不再疑惑

上一篇下一篇

猜你喜欢

热点阅读