前言
现在我们开始讲解Synchronized的底层实现.为什么上来就讲底层? 因为面试问的就很底层.
按照惯例,讲Synchronized必须要先讲解对象头,然后是锁升级过程,这也是面试重点考察的地方.其实我们也不光为了面试,最终目的还是为了帮助我们写出更加稳定高效的并发程序.越是复杂的知识体系,越要求对基础知识掌握通透.多线程学习更是这样,前期把每个知识点的底层和用法熟练掌握,才能对后期复杂的原理和故障排查做到心中有数,否则只能像个无头苍蝇一样乱飞乱撞,找不到方向.
对象的存储
锁在升级过程中要依赖对象头中的一些数据,通过对这些数据的修改来表示不同的状态.所以我们先熟悉一下对象在堆中的存储结构.当我们创建了一个对象时,会在堆空间分配一块区域,分配多大呢? 取决于对象所包含的内容.在JVM的堆中每个对象包含三部分内容:对象头,实例数据和填充数据.如果该对象是数组,那么对象头会额外包含一部分数据表示数组的长度
其中,填充(Padding)部分是可选的. 按照JVM规范,对象在堆中存储大小必须是字(word)的整数倍.在64位hotspot中,字长是8个字节.如果当前对象的大小不是字长的整数倍,那么使用填充部分补齐.
本系列所涉及的实验默认都是在64位Hotspot环境下进行,除非另有说明
JOL
如何查看对象在堆中的存储结构呢? 可以通过jol来查看.因为后面要多次用到该工具,所以先说明一下它的用法.
- 通过mavan添加jol的依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.14</version>
</dependency>
- 使用下面的代码就可以查看当前JVM的相关配置
public static void main(String[] args) {
System.out.println("Current VM details");
System.out.println(VM.current().details());
}
- 分析结果
Current VM details
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
- Running 64-bit HotSpot VM: 虚拟机类型
- Using compressed oop with 3-bit shift: 开启普通对象指针压缩
- Using compressed klass with 3-bit shift: 开启类型指针压缩
- Objects are 8 bytes aligned: 对象大小是8字节整数倍,如果不够使用填充来补充
- Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]: 八个基本类型的长度
- Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]: 数组元素八个基本类型的长度
为了节省空间,hotspot默认开启了指针压缩.如果不想压缩,可以通过
-XX:-UseCompressedOops来完成.
使用JOL查看对象
我们以普通对象为例,普通对象的对象头包含了mark word和class pointer.mark word包含了对象的哈希码,对象的分代年龄和锁状态标志.class pointer则是类型指针. 它们长度默认都是八个字节,因为上面开启了类型指针压缩,class pointer的长度被压缩位到四个字节.下面我们创建一个对象Person,然后看看它的内存结构
声明对象
public class Person {
private int age;
private String name;
private boolean married;
private String[] friends={"Lily","Kathy","Grace"};
public Person(int age,String name,boolean married){
this.age=age;
this.name=name;
this.married=married;
}
public void sayHi(){
System.out.printf("Hi everyone, i'm %s and %d year's old",this.name,this.age);
}
}
Person类包含了四个成员变量和一个实例方法.我们同样使用JOL进行查看
public static void main(String[] args) {
System.out.println("Current person details");
System.out.println(ClassLayout.parseInstance(person).toPrintable());
}
输出
Current person details
com.neal.learning.chapter7.Person 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) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int Person.age 24
16 1 boolean Person.married false
17 3 (alignment/padding gap)
20 4 java.lang.String Person.name (object)
24 4 java.lang.String[] Person.friends [(object), (object), (object)]
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total
结果分析
我们先看一下这行数据:
Instance size: 32 bytes
它表示person对象总共占用了32个字节.那这32是怎么得来的呢?:
- 对象头占12个字节
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) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
对象头包含了三行,其中前两行表示mark word,它占8个字节.最后一行表示class pointer,它本来也占8个字节,但是因为开启了类型指针压缩Using compressed klass with 3-bit shift 所以它被压缩成了4个字节.
注意: 这12个字节长度是固定的.也就是说对于任意对象,只要开启了类型指针压缩,那么对象头大小固定为12字节.
- 实例数据占16个字节
12 4 int Person.age 24
16 1 boolean Person.married false
17 3 (alignment/padding gap)
20 4 java.lang.String Person.name (object)
24 4 java.lang.String[] Person.friends [(object),
int类型age长度位4个字节,布尔类型married长度位4个字节(默认长度为1个字节,但是JVM会补充3个字节),String类型的name长度为4个字节,数组类型的friends长度为4个字节(它并不是数组的长度,而是指向数组的引用friends的长度). 这样实例数据的总的大小为4+(1+3)+4+4=16(Bytes)
3. 填充部分占用了4个字节
因为对象头大小为12个字节,实例数据大小为16个字节,两者之和并不能被word(8 Byte)整除,所以补充了4个字节.这样person实例共占有12+16+4=32 bytes
总结
我们学习了如何使用JOL,然后使用JOL查看了Person对象的内存结构并学会了如何观察结果.下一篇我们重点学习对象头,并结合JOL查看锁升级过程.