• Java对象内存布局



    title: Java对象内存布局
    date: 2021-05-05 15:58:39
    tags: Java;JVM

    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)

    不要用狭隘的眼光看待不了解的事物,自己没有涉及到的领域不要急于否定. 每天学习一点,努力过好平凡的生活.
  • 相关阅读:
    详解javascript中的闭包
    Cookie/Session的机制与安全
    session详解
    linux常用目录简介
    对比cp和scp命令 将数据从一台linux服务器复制到另一台linux服务器
    webpack打包速度和性能再次优化
    pc浏览器css和js计算浏览器宽度的差异以及和滚动条的关系
    chrome浏览器Timing内各字段解析
    深入理解-CLI与PHP-FPM
    swool教程链接汇总
  • 原文地址:https://www.cnblogs.com/GeekDanny/p/14733205.html
Copyright © 2020-2023  润新知