绕不开的"锁"(二)

174 阅读6分钟

前言

现在我们开始讲解Synchronized的底层实现.为什么上来就讲底层? 因为面试问的就很底层.

按照惯例,讲Synchronized必须要先讲解对象头,然后是锁升级过程,这也是面试重点考察的地方.其实我们也不光为了面试,最终目的还是为了帮助我们写出更加稳定高效的并发程序.越是复杂的知识体系,越要求对基础知识掌握通透.多线程学习更是这样,前期把每个知识点的底层和用法熟练掌握,才能对后期复杂的原理和故障排查做到心中有数,否则只能像个无头苍蝇一样乱飞乱撞,找不到方向.

对象的存储

锁在升级过程中要依赖对象头中的一些数据,通过对这些数据的修改来表示不同的状态.所以我们先熟悉一下对象在堆中的存储结构.当我们创建了一个对象时,会在堆空间分配一块区域,分配多大呢? 取决于对象所包含的内容.在JVM的堆中每个对象包含三部分内容:对象头,实例数据和填充数据.如果该对象是数组,那么对象头会额外包含一部分数据表示数组的长度

其中,填充(Padding)部分是可选的. 按照JVM规范,对象在堆中存储大小必须是字(word)的整数倍.在64位hotspot中,字长是8个字节.如果当前对象的大小不是字长的整数倍,那么使用填充部分补齐.

本系列所涉及的实验默认都是在64位Hotspot环境下进行,除非另有说明

JOL

如何查看对象在堆中的存储结构呢? 可以通过jol来查看.因为后面要多次用到该工具,所以先说明一下它的用法.

  1. 通过mavan添加jol的依赖
  <dependency>
     <groupId>org.openjdk.jol</groupId>
     <artifactId>jol-core</artifactId>
     <version>0.14</version>
  </dependency>
  1. 使用下面的代码就可以查看当前JVM的相关配置
  public static void main(String[] args) {
    System.out.println("Current VM details");
    System.out.println(VM.current().details());
  }
  1. 分析结果
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 wordclass 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是怎么得来的呢?:

  1. 对象头占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字节.

  1. 实例数据占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查看锁升级过程.