zoukankan      html  css  js  c++  java
  • 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)

    不要用狭隘的眼光看待不了解的事物,自己没有涉及到的领域不要急于否定. 每天学习一点,努力过好平凡的生活.
  • 相关阅读:
    TCP源码—连接建立
    TCP系列02—连接管理—1、三次握手与四次挥手
    TCP系列01—概述及协议头格式
    ubuntu软件管理apt与dpkg
    318. Maximum Product of Word Lengths
    317. Shortest Distance from All Buildings
    316. Remove Duplicate Letters
    315. Count of Smaller Numbers After Self
    314. Binary Tree Vertical Order Traversal
    313. Super Ugly Number
  • 原文地址:https://www.cnblogs.com/GeekDanny/p/14733205.html
Copyright © 2011-2022 走看看