java中对象在内存中的结构

对象的结构

在JVM中,一般来说,Java对象都是分配在堆中,那么对象在堆中长什么样呢?

在这里插入图片描述

对象头包含以下几个部分:

  • MarkWord:包含对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode、分代年龄等。
  • Class Pointer:一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例,默认开启压缩指针,占32位,关闭压缩,占64位。
  • 数组的长度:可选的,只有当对象是一个数组对象时才会有这个部分。
  • 对象的属性:占用内存空间取决于对象的属性数量和类型。
  • 对齐:为了保证对象头的字节数是8的倍数。

在这里插入图片描述

MarkWord

MarkWord用来存储线程的锁状态、hashcode、分代年龄等信息,当对象处于不同状态时,MarkWord中的数据也跟随着对象的状态而变化。

以上是Java对象处于5种不同状态时,Mark Word中64个bit的表现形式,上面每一行代表对象处于某种状态时的样子。

注意以下几点:

  • 偏向锁标志位+锁标志位共同表示当前对象锁的状态。
  • 分代年龄:在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC(除了CMS垃圾收集器)的年龄阈值为15,并发GC(CMS垃圾收集器)的年龄阈值为6。由于分代年龄只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold参数最大值为15的原因。
  • hashCode:31位的对象标识hashCode,采用延迟计算方式,只有在使用时才会计算,并会将结果写到该对象头中。如果对象在加锁前计算了hashcode,此时状态为无锁不可偏向,因为hashcode占用了偏向锁标志位的数据区域,这里的hashcode是指系统中最初始的hashcode,也就是调用基类Object.hashCode()方法产生的,而不是重写Object.hashCode()方法产生的hashcode,也可以使用System.identityHashCode(object)方法来获取。

项目中可以引入JOL(Java Object Layout)来打印java对象头在内存中的字节码。

		<dependency>
			<groupId>org.openjdk.jol</groupId>
			<artifactId>jol-core</artifactId>
			<version>0.10</version>
		</dependency>

下面通过代码对上面的5中状态进行验证:

无锁不可偏向

JVM刚启动时4s(默认值为4s,可以通过JVM参数-XX:BiasedLockingStartupDelay修改)之内创建的对象的锁状态为无锁不可偏向。

    @Test
    public void testNoLockByBeforeFiveSecond() {
    
    
        Object object = new Object();
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }

运行结果如下:

java.lang.Object 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)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

当一个对象计算了hashCode,锁的状态就会变成无锁不可偏向,因为hashcode占用了偏向锁标志位的数据区域。

    @Test
    public void testNoLockByHashCode() throws InterruptedException {
    
    
        TimeUnit.SECONDS.sleep(5); // 参数是4s,这里用休眠5s
        Object object = new Object();
        System.out.println(Integer.toHexString(object.hashCode()));
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }

运行结果如下:

5e8c92f4
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 f4 92 8c (00000001 11110100 10010010 10001100) (-1936526335)
      4     4        (object header)                           5e 00 00 00 (01011110 00000000 00000000 00000000) (94)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以发现对象的hashcode为5e8c92f4,与对象头中的hashcode一致。

匿名偏向锁

JVM启动4s后创建的对象锁的状态为匿名偏向锁。

    public void testAnonymousBiasLock() throws InterruptedException {
    
    
        TimeUnit.SECONDS.sleep(5); // 或者设置JVM args:-XX:BiasedLockingStartupDelay=0
        Object object = new Object();
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }

运行结果如下:

java.lang.Object 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)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

偏向锁

当没有发生资源的竞争,锁会偏向第一个持有锁的线程。

    @Test
    public void testBiasLock() throws InterruptedException {
    
    
        TimeUnit.SECONDS.sleep(5);
        Object object = new Object();
        synchronized (object) {
    
    
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
    }

运行结果如下:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 98 4e 00 (00000101 10011000 01001110 00000000) (5150725)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

注意此时对象头中会存有一个线程ID,但是此ID不是java中thread对象getId()方法返回的id,也不是操作系统中ID,这个ID是JVM生成的,可以通过jstack命令查看线程的ID。

$ jstack 16344
....
"main" #1 prio=5 os_prio=0 tid=0x0000000002239800 nid=0x4c18 in Object.wait() [0x000000000280e000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076b605d30> (a java.lang.Thread)
        at java.lang.Thread.join(Thread.java:1252)
        - locked <0x000000076b605d30> (a java.lang.Thread)
        at java.lang.Thread.join(Thread.java:1326)
        at com.morris.concurrent.syn.upgrade.SynchronizedStatus.testBiasLock(SynchronizedStatus.java:74)
....

tid对应对象头中的线程id,而nid对应操作系统级别的线程id。

轻量级锁

当对象头中的锁偏向于线程T1,T1释放锁后,其他线程来获取,此时偏向锁会升级为轻量级锁(线程之间交替执行,没有资源竞争)。

    /**
     * 偏向锁 -> 轻量级锁
     */
    @Test
    public void testLightLock() throws InterruptedException {
    
    
        TimeUnit.SECONDS.sleep(5);
        Object object = new Object();

        Thread t = new Thread(() -> {
    
    
            synchronized (object) {
    
    
            }
        });
        t.start();
        t.join();

        synchronized (object) {
    
    
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }

        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }

运行结果如下:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           f0 e0 7c 02 (11110000 11100000 01111100 00000010) (41738480)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     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.lang.Object 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)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

当JVM刚启动或者使用JVM参数-XX:-UseBiasedLocking=false禁用了偏向锁,这时无锁不可偏向锁就会升级为轻量级锁。

    @Test
    public void testLightLock2() throws InterruptedException {
    
    
        Object object = new Object();
        synchronized (object) {
    
    
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
    }

运行结果如下:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           88 e5 7a 02 (10001000 11100101 01111010 00000010) (41608584)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

注意轻量级锁释放后,对象头中锁的状态会变为无锁不可偏向。

重量级锁

当产生了资源竞争,轻量级锁会升级为重量级锁。

    @Test
    public void testHeavyLock() throws InterruptedException {
    
    
        Object object = new Object();

        Thread t = new Thread(() -> {
    
    
            synchronized (object) {
    
    
            }
        });
        t.start();

        synchronized (object) {
    
    
            TimeUnit.SECONDS.sleep(1);
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
    }

运行结果如下:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           fa c1 ec 1b (11111010 11000001 11101100 00011011) (468500986)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Class Pointer

JVM通过这个指针确定对象是哪个类的实例,该指针的长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。

如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,默认开启,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

  • 每个Class的属性指针(即静态变量)
  • 每个对象的属性指针(即对象变量)
  • 普通对象数组的每个元素指针

当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如JDK8中指向元空间的Class对象指针、本地变量、堆栈元素、入参、返回值和NULL指针等。

问题:Object object = new Object()中object对象到底占用多少个字节?

答案:16个字节。

开启压缩指针:markword(64bit) + classpointer(32bit) + 对齐(32bit) = 16byte 。

不开启压缩指针:markword(64bit) + classpointer(64bit) = 16byte 。

猜你喜欢

转载自blog.csdn.net/u022812849/article/details/108366356