Java对象内存布局

Java对象内存布局

  • 引子

  • 运行时数据区域

  • 虚拟机对象

  • 锁升级(Synchronized)

1.引子

Java与C++之间隔着一堵由内存分配和垃圾回收筑城围墙,墙外面的人想进去,墙里面的人想出来。
正是因为Java虚拟机的内存分配和垃圾回收机制,减轻了程序员在编码时内存分配的负担,可以把更多精力放在实现上。
任何事物都有利弊,享受Java虚拟机的便利,就要承担相应的风险。
当Java程序内存出现泄漏的时候,如果没有搞懂虚拟机的内存分配及对象内存布局,就像隔靴搔痒,很难排查问题。

2.运行时数据区域

  • 程序计数器 pcRegister
当前线程所执行字节码的行号指示器
  • Java虚拟机栈 VM Stack
Java方法执行的线程内存模型,方法执行就会创建栈帧Stack Frame
  • 本地方法栈 Native Stack
使用Native本地方法是创建
  • Java堆 Java Heap
所有的对象实例以及数组都应在堆上分配
  • 方法区 Method Area
存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
  • 运行常量池 Runtime Constant Pool
运行常量池是方法区的一部分;常量池表用于存放编译期生成的各种字面量与符号引用
  • 直接内存 Direct Memory
NIO 基于Channel和Buffer的I/O方式,使用Native函数直接分配堆外内存。
利用Java堆中的DirectByteBuffer对象作为这块对象的内存引用进行操作,避免在Java堆和Native堆来回复制数据

3.虚拟机对象

Java对象的创建方式

  • new (new + invokespecial)
  • clone (implements java.lang.Cloneable)
  • 反射
  • 反序列化(implements Serializable)

对象初始化

  • 实例变量初始化
  • 实例代码块初始化
  • 构造函数初始化
Java对象初始化过程中,主要涉及三种执行对象初始化的结构,
分别是 实例变量初始化、实例代码块初始化 以及 构造函数初始化

对象的内部布局

对象在堆内存中的存储布局:

  • 对象头
  • 实例数据
  • 对齐填充

//导入依赖工具jol可以查看对象的内存布局
import org.openjdk.jol.info.ClassLayout;
public class jolTest {
    public static class UserTest{
    }
    public static void main(String[] args) {
        UserTest userTest = new UserTest();
        System.out.println(ClassLayout.parseInstance(userTest).toPrintable());
    }
}
/*
jolTest$UserTest 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        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
*/

// 16B = 8B(mark word) + 4B(klass pointer) +0B(instance data)+4B (padding)
// kclass pointer 4B是开启指针压缩
<!--JOL 查看对象内存布局-->
<dependency>
   <groupId>org.openjdk.jol</groupId>
   <artifactId>jol-core</artifactId>
   <version>0.14</version>
</dependency>
  • MarkWord

4.锁升级

jdk6之前,synchronized关键字加锁是无差别的重量级锁

锁升级:偏向锁,轻量级锁,重量级锁
  
如上图,锁在markword中占3bit:
偏向锁位(biased_lock)1bit + 锁标志位(lock)2bit 
  
  • 1.锁对象刚创建,没有任何线程竞争,对象处于无锁状态+不可偏向状态

    大小端转换: 00000001

    偏向锁位(biased_lock)1bit + 锁标志位(lock)2bit = 0 01

jolTest$UserTest 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        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

​ jdk中偏向锁存在延迟4秒启动,也就是说在jvm启动后4秒后创建的对象才会开启偏向锁,

可以通过jvm参数取消这个延迟时间

​ 创建的对象状态为 对象处于无锁状态+可偏向状态

​ 偏向锁位(biased_lock)1bit + 锁标志位(lock)2bit = 1 01

// -XX:BiasedLockingStartupDelay=0
jolTest$UserTest 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)     43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
  • 2.在没有线程竞争的条件下,第一个获取锁的线程通过CAS将自己的threadId写入到该对象的mark word中,若后续该线程再次获取锁,需要比较当前线程threadId和对象mark word中的threadId是否一致,如果一致那么可以直接获取,并且锁对象始终保持对该线程的偏向,也就是说偏向锁不会主动释放
public static void main(String[] args) {
        UserTest userTest = new UserTest();

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

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

// terminal print
first get synchronized lock
 OFFSET  SIZE   TYPE DESCRIPTION       VALUE
      0     4        (object header)   05 88 80 72 (00000101 10001000 10000000 01110010) (1921026053)
------------------------------------------------
unlock
 OFFSET  SIZE   TYPE DESCRIPTION       VALUE
      0     4        (object header)   05 88 80 72 (00000101 10001000 10000000 01110010) (1921026053)
------------------------------------------------
second get synchronized lock
 OFFSET  SIZE   TYPE DESCRIPTION       VALUE
      0     4        (object header)   05 88 80 72 (00000101 10001000 10000000 01110010) (1921026053)
  • 3.当两个或以上线程交替获取锁,但并没有在对象上并发的获取锁时,偏向锁升级为轻量级锁。在此阶段,线程采取CAS的自旋方式尝试获取锁,避免阻塞线程造成的cpu在用户态和内核态间转换的消耗
主线程首先对user对象加锁,首次加锁为101偏向锁
子线程等待主线程释放锁后,对user对象加锁,这时将偏向锁升级为00轻量级锁
轻量级锁解锁后,user对象无线程竞争,恢复为001无锁态,并且处于不可偏向状态。如果之后有线程再尝试获取user对象的锁,会直接加轻量级锁,而不是偏向锁
public static void main(String[] args) throws InterruptedException {

        final UserTest userTest = new UserTest();

        synchronized (userTest){
            System.out.println("Main = "+ClassLayout.parseInstance(userTest).toPrintable());
        }
        Thread thread = new Thread(new Runnable() {
            public void run() {
                synchronized (userTest){
                    System.out.println("Thread = "+ClassLayout.parseInstance(userTest).toPrintable());
                }
            }
        });
        thread.start();
        thread.join();
        System.out.println("End = "+ClassLayout.parseInstance(userTest).toPrintable());
    }
//terminal print
Main = jolTest$UserTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION       VALUE
      0     4        (object header)   05 90 80 e3 (00000101 10010000 10000000 11100011) (-478113787)
Thread = jolTest$UserTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION       VALUE
      0     4        (object header)   60 a9 d4 03 (01100000 10101001 11010100 00000011) (64268640)

End = jolTest$UserTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION       VALUE
      0     4        (object header)   01 00 00 00 (00000001 00000000 00000000 00000000) (1)
  • 4.两个或以上线程并发的在同一个对象上进行同步时,为了避免无用自旋消耗cpu,轻量级锁会升级成重量级锁。这时mark word中的指针指向的是monitor对象(也被称为管程或监视器锁)的起始地址
        new Thread(new Runnable() {
            public void run() {
                synchronized (userTest){
                    System.out.println("Thread1 = "+ClassLayout.parseInstance(userTest).toPrintable());
                    try{
                        TimeUnit.SECONDS.sleep(2);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                synchronized (userTest){
                    System.out.println("Thread2 = "+ClassLayout.parseInstance(userTest).toPrintable());
                    try{
                        TimeUnit.SECONDS.sleep(2);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        try{
            TimeUnit.SECONDS.sleep(4);
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        System.out.println("main:"+ClassLayout.parseInstance(userTest).toPrintable());

// Teriminal print
Thread1 = jolTest$UserTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION      VALUE
      0     4        (object header)  fa 49 85 97 (11111010 01001001 10000101 10010111) (-1752872454)
Thread2 = jolTest$UserTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION      VALUE
      0     4        (object header)  fa 49 85 97 (11111010 01001001 10000101 10010111) (-1752872454)
main:jolTest$UserTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION      VALUE
      0     4        (object header)  fa 49 85 97 (11111010 01001001 10000101 10010111) (-1752872454)

指针压缩

Klass Pointer 类型指针,jdk6之后默认开启指针压缩
#开启指针压缩:
-XX:+UseCompressedOops
#关闭指针压缩:
-XX:-UseCompressedOops
  
//关闭指针压缩后
Thread1 = jolTest$UserTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION         VALUE
      0     4        (object header)     5a d1 81 4b (01011010 11010001 10000001 01001011) (1266798938)
      4     4        (object header)     cb 7f 00 00 (11001011 01111111 00000000 00000000) (32715)
      8     4        (object header)     b0 37 40 a3 (10110000 00110111 01000000 10100011) (-1556072528)
     12     4        (object header)     01 00 00 00 (00000001 00000000 00000000 00000000) (1)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
  • 实现逻辑
由于使用了8字节对齐后每个对象的地址偏移量后3位必定为0,所以在存储的时候可以将后3位0抹除(
转化为bit是抹除了最后24位),在此基础上再去掉最高位,就完成了指针从8字节到4字节的压缩。
而在实际使用时,在压缩后的指针后加3位0,就能够实现向真实地址的映射

数组长度

public static void main(String[] args) throws InterruptedException {

        UserTest[] userTests = new UserTest[2];

        System.out.println("main:"+ClassLayout.parseInstance(userTests).toPrintable());


}

main:[LjolTest$UserTest; 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)     81 c1 00 f8 (10000001 11000001 00000000 11111000) (-134168191)
     12     4      (object header)     02 00 00 00 (00000010 00000000 00000000 00000000) (2)
     16     8   jolTest$UserTest [LjolTest$UserTest;.<elements>            N/A
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
8字节mark word
4字节klass pointer
4字节数组长度,值为2,表示数组中有两个元素
开启指针压缩后每个引用类型占4字节,数组中两个元素共占8字节

参考

1.图文详解Java对象内存布局-码农参上

2.深入理解Java虚拟机(第三版)- 周志华

3.[深入理解Java对象的创建过程:类的初始化与实例化-书呆子Rico](https://blog.csdn.net/justloveyou_/article/details/72466416?utm_medium=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromMachineLearnPai2~default-3.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromMachineLearnPai2~default-3.control)

posted @ 2021-05-05 22:49  dengshuo7412  阅读(133)  评论(0编辑  收藏  举报