Java内存布局

Java对象内存布局

在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:

  • 对象头(Header)

  • 实例数据(Instance Data)

  • 对齐填充(Padding)

对象头:HotSpot虚拟机对象的对象头包括三部分信息。

  1. 第一部分是用来存储对象自身的运行时数据。如哈希码(HashCode)分代年龄锁状态标志线程持有的锁偏向线程ID偏向时间戳等,这部分数据的长度在32bit和64bit的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“Mark Word”。

    Java对象头里的Mark Word里默认存储对象的HashCode分代年龄锁标记位。以32bit的HotSpot虚拟机为例,如对象处于无锁状态下,各状态如下所示

在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据

Java内存布局_第1张图片

在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下所示

  1. 第二部分是类型指针。通过这个指针来确定该对象是哪个类的实例。jdk1.8默认开启指针压缩后为4字节,当在JVM参数中关闭指针压缩(-XX:-UseCompressedOops)后,长度为8字节。
  2. 第三部分存储数组长度,只有对象为数组时才会存在。

可以看出,在64位虚拟机中,一个对象头的长度最小为8字节(MarkWord)+ 4字节(类型指针,默认开启的指针压缩)= 12个字节。而对象的长度必须为8字节的整数倍,那么就必须还有4字节的填充数据,那么一个对象就最少有16个字节的长度。

实例数据:对象真正存储的有效信息

对齐填充:由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是
任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者
2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

查看内存布局

通过工具可以分析下对象的具体结构,依赖地址如下

<dependency>
    <groupId>org.openjdk.jolgroupId>
    <artifactId>jol-coreartifactId>
    <version>0.12version>
dependency>

编写代码

public class T {
     
    public static void main(String[] args) {
     
        T obj = new T();
        System.out.println(obj);
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}

控制台打印结果

com.example.demo.test.T@7106e68e
com.example.demo.test.T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 8e e6 06 (00000001 10001110 11100110 00000110) (115772929)
      4     4        (object header)                           71 00 00 00 (01110001 00000000 00000000 00000000) (113)
      8     4        (object header)                           05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

从打印结果可以看出该对象一共占有16字节(对象头占8字节+类型指针占4字节+填充数据占4字节)。
由于jdk1.8默认开启指针压缩,所以类型指针只占4字节,通过虚拟机参数(-XX:-UseCompressedOops)关闭指针压缩再看下效果

com.example.demo.test.T@7106e68e
com.example.demo.test.T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 8e e6 06 (00000001 10001110 11100110 00000110) (115772929)
      4     4        (object header)                           71 00 00 00 (01110001 00000000 00000000 00000000) (113)
      8     4        (object header)                           28 30 c5 17 (00101000 00110000 11000101 00010111) (398798888)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

这个时候对象的结构只剩下对象头信息了,MarkWord还是8个字节,而类型指针变成了8字节。

为了看到实例数据,我们需要在对象中定义一些变量,修改代码如下

public class T {
     
    public int m1 = 123;
    public long m2 = 456L;
    public String m3 = "789";
    public static void main(String[] args) {
     
        T obj = new T();
        System.out.println(obj);
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}

控制台打印结果

com.example.demo.test.T@7106e68e
com.example.demo.test.T object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 8e e6 06 (00000001 10001110 11100110 00000110) (115772929)
      4     4                    (object header)                           71 00 00 00 (01110001 00000000 00000000 00000000) (113)
      8     4                    (object header)                           05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4                int T.m1                                      123
     16     8               long T.m2                                      456
     24     4   java.lang.String T.m3                                      (object)
     28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以发现,实例数据由m1、m2、m3组成,分别占用4字节、8字节、4字节。
代码中打印了该对象的hashCode为7106e68e,该对象的MarkWord信息如下(可以发现,这里的取值是倒过来的):

00000000 00000000 00000000 01110001 00000110 11100110 10001110 00000001
(00 00 00 71 06 e6 8e 01)

可以看到,最后的3bit(1bit标识偏向锁,2bit描述锁的类型)是跟锁相关的,而Synchronized的锁优化升级就是修改的这几位上的标识用来区分不同的锁,从而采取不同的策略来提升性能。

锁的升级与对比

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

偏向锁

HotSpo的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程;如果没有设置,则使用CAS竞争锁。

一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束。根据锁对象目前是
否处于被锁定的状态决定是否撤销偏向(偏向模式设置为“0”),撤销后标志位恢复到未锁定(标志位
为“01”)或轻量级锁定(标志位为“00”)的状态。

偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。

// 如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0
public class T {
     
    public static void main(String[] args) throws InterruptedException {
     
        System.out.println(ClassLayout.parseInstance(new T()).toPrintable());
        // 
        Thread.sleep(5*1000);
        System.out.println(ClassLayout.parseInstance(new T()).toPrintable());
    }
}

运行结果如下,可以发现程序启动后睡眠5秒,对象的偏向锁就打开了。

com.example.demo.test.T 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)                           05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

com.example.demo.test.T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

轻量级锁

(1)轻量级锁加锁线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态值变为“10”,此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。

(2)轻量级锁解锁轻量级解锁时,会使用原子的CAS操作将Displaced MarkWord替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

Java内存布局_第2张图片

重量级锁

当系统检查到锁是重量级锁之后,将想要获得锁的线程进行阻塞,被阻塞的线程不会消耗CPU。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。

优缺点

Java内存布局_第3张图片

实验

/**
 * 关闭偏向锁的延迟
 * -XX:BiasedLockingStartupDelay=0
 */
public class T {
     
    public static void main(String[] args) throws InterruptedException {
     
        T obj = new T();
        System.out.println("########## " + Thread.currentThread().getName() + " ##########");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());

        new Thread(() -> {
     
            synchronized (obj) {
     
                System.out.println("########## " + Thread.currentThread().getName() + " ##########");
                System.out.println(ClassLayout.parseInstance(obj).toPrintable());
            }
        },"A线程").start();

        new Thread(() -> {
     
            synchronized (obj) {
     
                System.out.println("########## " + Thread.currentThread().getName() + " ##########");
                System.out.println(ClassLayout.parseInstance(obj).toPrintable());
            }
        },"B线程").start();
    }
}

运行结果可能出现以下几种情况

偏向锁->偏向锁->轻量级锁

偏向锁->偏向锁->轻量级锁,这个情况需要多运行几次才会出现

########## main ##########
com.example.demo.test.T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

########## A线程 ##########
com.example.demo.test.T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 d8 29 19 (00000101 11011000 00101001 00011001) (422172677)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

########## B线程 ##########
com.example.demo.test.T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           70 ef 1c 1a (01110000 11101111 00011100 00011010) (438103920)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

偏向锁->轻量级锁->重量级锁

偏向锁->轻量级锁->重量级,这个情况需要多运行几次才会出现

com.example.demo.test.T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

########## A线程 ##########
com.example.demo.test.T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           90 f0 d7 1a (10010000 11110000 11010111 00011010) (450359440)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

########## B线程 ##########
com.example.demo.test.T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           ba 8c 74 03 (10111010 10001100 01110100 00000011) (57969850)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

偏向锁->偏向锁->重量级锁

########## main ##########
com.example.demo.test.T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

########## A线程 ##########
com.example.demo.test.T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 08 bb 19 (00000101 00001000 10111011 00011001) (431687685)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

########## B线程 ##########
com.example.demo.test.T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           0a cb f1 17 (00001010 11001011 11110001 00010111) (401722122)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

偏向锁->重量级锁->重量级锁

########## main ##########
com.example.demo.test.T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

########## A线程 ##########
com.example.demo.test.T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           2a 8c 9e 03 (00101010 10001100 10011110 00000011) (60722218)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

########## B线程 ##########
com.example.demo.test.T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           2a 8c 9e 03 (00101010 10001100 10011110 00000011) (60722218)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

你可能感兴趣的:(Java)