精华内容
下载资源
问答
  • java对象内存布局
    2022-01-04 15:59:49

    一、Java对象内存布局

    Hotspot虚拟机的java对象的内存由以下几部分:
    (1)对象头(Mark word / Klass Pointer / 数组长度)
    (2)实例数据
    (3)对齐填充数据

    1.1. 对象头

    • 对象头中的Mark word:是用于存储对象自身运行时的数据,占用8字节。例如:hashcode、GC分代、锁状态标志、偏向锁线程ID、偏向时间戳等。
    • 对象头中Klass Pointer:对象指向它的类的元素局的指针,虚拟机通过这个指针类确定这个对象是哪个类的实例,占用8字节。(数组,对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通java对象的元数据信息确定java对象的大小,但是从数组的元数据中无法确定数组的大小)
    • 对象头中的 数组长度:如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。

    64位JVM中,Mark word内存布局,占用64位空间,也就是8字节;
    32位JVM中,Mark word内存布局,占用32位空间,也就是4字节;
    64位JVM对象内存布局
    在这里插入图片描述

    1.2. 数据实例

    就是类中定义的成员变量属性。

    类型占用内存(字节)
    boolean1
    byte1
    short2
    char2
    int4
    float4
    long8
    double8
    数组引用开启压缩就占用4 byte,关闭压缩就占用8 byte
    对象引用开启压缩就占用4 byte,关闭压缩就占用8 byte

    reference类型在32位系统上每个占用4bytes, 在64位系统上每个占用8bytes。

    1.3.对齐填充数据

    对齐填充并不是必然存在的,也没有特定的含义,仅仅骑着占位符的作用。
    由于Hotspot虚拟机的自动内存管理系统要求对象的其实地址必须是 8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数,因此,对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。

    1.4.指针压缩

    对象占用的内存大小收到VM参数UseCompressedOops的影响。java运行参数上使用 -XX:+UseCompressedOops 就是开启指针压缩;使用 -XX:-UseCompressedOops 就是关闭指针压缩

    1.4.1. 对对象头的影响

    开启压缩后,对象头大小为12bytes(64位虚拟机)。

    1.4.2. 对引用类型的影响

    开启压缩就占用4 byte,关闭压缩就占用8 byte(64位虚拟机)

    二、Monitor监听器

    源码路径:…\src\share\vm\runtime\objectMonitor.hpp

      ObjectMonitor() {
        _header       = NULL;
        _count        = 0;		// 记录个数
        _waiters      = 0,
        _recursions   = 0;		// 重入次数
        _object       = NULL;	// 储存monitor关联对象
        _owner        = NULL;	// 储存当前持有锁的线程ID
        _WaitSet      = NULL;	// 等待池:通过调用 wait(),将当前线程变为阻塞状态放到等待池
        _WaitSetLock  = 0 ;
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ;	// 多线程竞争锁时的单向链表
        FreeNext      = NULL ;
        _EntryList    = NULL ;	// 锁池:处于等待锁block状态的线程,会被加入到该列表,如果被调用了notify(),则会将当前线程从 _WaitSet 移到 _EntryList
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ;
        _previous_owner_tid = 0;
      }
    

    重量锁竞争流程

    假设T1T2T3T4 同时使用synchronize竞争锁时

    1. T1T2T3T4 会先加入到_cxq 中参与竞争锁
    2. T1竞争到锁,则会修改该对象的对象头的_owner,并将其他线程T2T3T4进入到锁池 _EntryList
    3. T1调用了wait()方法,会修改线程变为阻塞、释放对象锁,并将当前线程T1加入到等待池_WaitSet中,然后再将T2T3T4加入到_cxq中,进行下一轮锁的竞争
    4. T1已加入到等待池_WaitSet中,然后T2 对对象锁调用notify() / notifyAll()唤醒等待时,则T1会从等待池_WaitSet 转移到锁池 _EntryList,等待下一轮的锁竞争
    5. T1执行完成,准备释放锁,会移除_owner,并将T2T3T4从锁池 _EntryList转移到竞争队列_cxq中,进行下一轮锁的竞争
    更多相关内容
  • Java对象内存布局

    2022-03-21 11:16:06
    内存规整则使用指针碰撞方式:所有用过的内存放在一边,没有使用过的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存就是把指针向空闲的内存空间移动与对象大小相等的距离 内存不规整则使用空闲列表...

    对象的创建

    内存分配

    • 内存规整则使用指针碰撞方式:所有用过的内存放在一边,没有使用过的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存就是把指针向空闲的内存空间移动与对象大小相等的距离
    • 内存不规整则使用空闲列表方式:内存空间相互交错,虚拟机就必须维护一个列表,记录哪些内存区域是可用的,在分配时从列表找到一块足够大的内存分配给对象实例,并更新列表上的记录

    内存分配线程安全

    虚拟机创建对象是非常频繁的操作,有可能出现正在给A分配内存,还未来得及移动指针,对象B同时使用了原来的指针来分配内存的情况。

    1. 对分配内存空间的动作进行同步处理,实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
    2. 另一种方案是把内存分配的动作按照线程划分在不同的空间之中执行,每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓存,哪个线程需要分配内存就在哪个线程本地线程分配缓存上分配,只有线程本地线程分配缓存用完并分配新的缓存时,才需要同步锁定

    对象的内存布局

    对象头

    第一部分用于存储对象自身的数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。在32位和64位虚拟机中大小分别为32bit和64bit,官方称之为Mark Wolrd

    第二部分是类型指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 

    对象头又包括三部分:MarkWord、元数据指针、数组长度。

    • MarkWord:用于存储对象运行时的数据,好比 HashCode、锁状态标志、GC分代年龄等。这部分在 64 位操作系统下占 8 字节,32 位操作系统下占 4 字节。
    • 指针:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪一个类的实例。
      这部分就涉及到指针压缩的概念,在开启指针压缩的状况下占 4 字节,未开启状况下占 8 字节。
    • 数组长度:这部分只有是数组对象才有,若是是非数组对象就没这部分。这部分占 4 字节

    实例数据

    储存对象实例数据,也是在程序代码中定义的各种类型的字段内容,无论是父类还是子类都会记录

    对齐填充

    没有特殊含义,仅仅起着占位符的作用。

    那么为何非要进行 8 字节对齐呢?这样岂不是浪费了空间资源?

    由于 CPU 进行内存访问时,一次寻址的指针大小是 8 字节,正好也是 L1 缓存行的大小。如果不进行内存对齐,则可能出现跨缓存行的情况,这叫做 缓存行污染

    对象的访问定位

    句柄访问

     Java堆中将会划分一快内存来作为句柄池,虚拟机栈中的引用存储的就是对象的句柄池地址,句柄中包含了对象的实例数据与类型数据各自的具体地址,在对象移动的时候只更改句柄池内地址,引用本身不需要更改。

    指针访问

    虚拟机栈引用存储的直接就是对象地址,指针访问速度更快,节省了一次指针定位的时间

    展开全文
  • 对象的创建方式虽然有很多,可以通过new、反射、clone、反序列化等不同方式来创建,但最终使用时对象都要被放到内存中,那么你知道在内存中的java对象是由哪些部分组成、又是怎么存储的吗? 本文将基于代码进行实例...

    作为一名Java程序员,我们在日常工作中使用这款面向对象的编程语言时,做的最频繁的操作大概就是去创建一个个的对象了。对象的创建方式虽然有很多,可以通过new、反射、clone、反序列化等不同方式来创建,但最终使用时对象都要被放到内存中,那么你知道在内存中的java对象是由哪些部分组成、又是怎么存储的吗?

    本文将基于代码进行实例测试,详细探讨对象在内存中的组成结构。全文目录结构如下:

    • 1、对象内存结构概述

    • 2、JOL 工具简介

    • 3、对象头

    • 4、实例数据

    • 5、对齐填充字节

    • 6、总结

    文中代码基于 JDK 1.8.0_261,64-Bit HotSpot 运行

    1、对象内存结构概述

    在介绍对象在内存中的组成结构前,我们先简要回顾一个对象的创建过程:

    1、jvm将对象所在的class文件加载到方法区中

    2、jvm读取main方法入口,将main方法入栈,执行创建对象代码

    3、在main方法的栈内存中分配对象的引用,在堆中分配内存放入创建的对象,并将栈中的引用指向堆中的对象

    所以当对象在实例化完成之后,是被存放在堆内存中的,这里的对象由3部分组成,如下图所示:

    对各个组成部分的功能简要进行说明:

    • 对象头:对象头存储的是对象在运行时状态的相关信息、指向该对象所属类的元数据的指针,如果对象是数组对象那么还会额外存储对象的数组长度

    • 实例数据:实例数据存储的是对象的真正有效数据,也就是各个属性字段的值,如果在拥有父类的情况下,还会包含父类的字段。字段的存储顺序会受到数据类型长度、以及虚拟机的分配策略的影响

    • 对齐填充字节:在java对象中,需要对齐填充字节的原因是,64位的jvm中对象的大小被要求向8字节对齐,因此当对象的长度不足8字节的整数倍时,需要在对象中进行填充操作。注意图中对齐填充部分使用了虚线,这是因为填充字节并不是固定存在的部分,这点在后面计算对象大小时具体进行说明

    2、JOL 工具简介

    在具体开始研究对象的内存结构之前,先介绍一下我们要用到的工具,openjdk官网提供了查看对象内存布局的工具jol (java object layout),可在maven中引入坐标:

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

    在代码中使用jol提供的方法查看jvm信息:

    System.out.println(VM.current().details());
    

    通过打印出来的信息,可以看到我们使用的是64位 jvm,并开启了指针压缩,对象默认使用8字节对齐方式。通过jol查看对象内存布局的方法,将在后面的例子中具体展示,下面开始对象内存布局的正式学习。

    3、对象头

    首先看一下对象头(Object header)的组成部分,根据普通对象和数组对象的不同,结构将会有所不同。只有当对象是数组对象才会有数组长度部分,普通对象没有该部分,如下图所示:

    在对象头中mark word 占8字节,默认开启指针压缩的情况下klass pointer 占4字节,数组对象的数组长度占4字节。在了解了对象头的基础结构后,现在以一个不包含任何属性的空对象为例,查看一下它的内存布局,创建User类:

    public class User {
    }
    

    使用jol查看对象头的内存布局:

    public static void main(String[] args) {
        User user=new User();
        //查看对象的内存布局
        System.out.println(ClassLayout.parseInstance(user).toPrintable());
    }
    

    执行代码,查看打印信息:

    • OFFSET:偏移地址,单位为字节

    • SIZE:占用内存大小,单位为字节

    • TYPEClass中定义的类型

    • DESCRIPTION:类型描述,Obejct header 表示对象头,alignment表示对齐填充

    • VALUE:对应内存中存储的值

    当前对象共占用16字节,因为8字节标记字加4字节的类型指针,不满足向8字节对齐,因此需要填充4个字节:

    8B (mark word) + 4B (klass pointer) + 0B (instance data) + 4B (padding)
    

    这样我们就通过直观的方式,了解了一个不包含属性的最简单的空对象,在内存中的基本组成是怎样的。在此基础上,我们来深入学习对象头中各个组成部分。

    3.1 Mark Word 标记字

    在对象头中,mark word 一共有64个bit,用于存储对象自身的运行时数据,标记对象处于以下5种状态中的某一种:

    3.1.1 基于mark word的锁升级

    在jdk6 之前,通过synchronized关键字加锁时使用无差别的的重量级锁,重量级锁会造成线程的串行执行,并且使cpu在用户态和核心态之间频繁切换。随着对synchronized的不断优化,提出了锁升级的概念,并引入了偏向锁、轻量级锁、重量级锁。在mark word中,锁(lock)标志位占用2个bit,结合1个bit偏向锁(biased_lock)标志位,这样通过倒数的3位,就能用来标识当前对象持有的锁的状态,并判断出其余位存储的是什么信息。

    基于mark word的锁升级的流程如下:

    1、锁对象刚创建时,没有任何线程竞争,对象处于无锁状态。在上面打印的空对象的内存布局中,根据大小端,得到最后8位是00000001,表示处于无锁态,并且处于不可偏向状态。这是因为在jdk中偏向锁存在延迟4秒启动,也就是说在jvm启动后4秒后创建的对象才会开启偏向锁,我们通过jvm参数取消这个延迟时间:

    -XX:BiasedLockingStartupDelay=0
    

    这时最后3位为101,表示当前对象的锁没有被持有,并且处于可被偏向状态。

    2、在没有线程竞争的条件下,第一个获取锁的线程通过CAS将自己的threadId写入到该对象的mark word中,若后续该线程再次获取锁,需要比较当前线程threadId和对象mark word中的threadId是否一致,如果一致那么可以直接获取,并且锁对象始终保持对该线程的偏向,也就是说偏向锁不会主动释放。

    使用代码进行测试同一个线程重复获取锁的过程:

    public static void main(String[] args) {
        User user=new User();
        synchronized (user){
            System.out.println(ClassLayout.parseInstance(user).toPrintable());
        }
        System.out.println(ClassLayout.parseInstance(user).toPrintable());
        synchronized (user){
            System.out.println(ClassLayout.parseInstance(user).toPrintable());
        }
    }
    

    执行结果:

    可以看到一个线程对一个对象加锁、解锁、重新获取对象的锁时,mark word都没有发生变化,偏向锁中的当前线程指针始终指向同一个线程。

    3、当两个或以上线程交替获取锁,但并没有在对象上并发的获取锁时,偏向锁升级为轻量级锁。在此阶段,线程采取CAS的自旋方式尝试获取锁,避免阻塞线程造成的cpu在用户态和内核态间转换的消耗。测试代码如下:

    public static void main(String[] args) throws InterruptedException {
        User user=new User();
        synchronized (user){
            System.out.println("--MAIN--:"+ClassLayout.parseInstance(user).toPrintable());
        }
    
        Thread thread = new Thread(() -> {
            synchronized (user) {
                System.out.println("--THREAD--:"+ClassLayout.parseInstance(user).toPrintable());
            }
        });
        thread.start();
        thread.join();
        System.out.println("--END--:"+ClassLayout.parseInstance(user).toPrintable());
    }
    

    先直接看一下结果:

    整个加锁状态的变化流程如下:

    • 主线程首先对user对象加锁,首次加锁为101偏向锁

    • 子线程等待主线程释放锁后,对user对象加锁,这时将偏向锁升级为00轻量级锁

    • 轻量级锁解锁后,user对象无线程竞争,恢复为001无锁态,并且处于不可偏向状态。如果之后有线程再尝试获取user对象的锁,会直接加轻量级锁,而不是偏向锁

    4、当两个或以上线程并发的在同一个对象上进行同步时,为了避免无用自旋消耗cpu,轻量级锁会升级成重量级锁。这时mark word中的指针指向的是monitor对象(也被称为管程或监视器锁)的起始地址。测试代码如下:

    public static void main(String[] args) {
        User user = new User();
        new Thread(() -> {
            synchronized (user) {
                System.out.println("--THREAD1--:" + ClassLayout.parseInstance(user).toPrintable());
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(() -> {
            synchronized (user) {
                System.out.println("--THREAD2--:" + ClassLayout.parseInstance(user).toPrintable());
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
    

    查看结果:

    可以看到,在两个线程同时竞争user对象的锁时,会升级为10重量级锁。

    3.1.2 其他信息

    mark word 中其他重要信息进行说明:

    • hashcode:无锁态下的hashcode采用了延迟加载技术,在第一次调用hashCode()方法时才会计算写入。对这一过程进行验证:

    public static void main(String[] args) {
        User user=new User();
        //打印内存布局
        System.out.println(ClassLayout.parseInstance(user).toPrintable());
        //计算hashCode
        System.out.println(user.hashCode());
        //再次打印内存布局
        System.out.println(ClassLayout.parseInstance(user).toPrintable());
    }
    

    可以看到,在没有调用hashCode()方法前,31位的哈希值不存在,全部填充为0。在调用方法后,根据大小端,被填充的数据为:

    1011001001101100011010010101101
    

    将2进制转换为10进制,对应哈希值1496724653。需要注意,只有在调用没有被重写的Object.hashCode()方法或System.identityHashCode(Object)方法才会写入mark word,执行用户自定义的hashCode()方法不会被写入。

    大家可能会注意到,当对象被加锁后,mark word中就没有足够空间来保存hashCode了,这时hashcode会被移动到重量级锁的Object Monitor中。

    • epoch:偏向锁的时间戳

    • 分代年龄(age):在jvm的垃圾回收过程中,每当对象经过一次Young GC,年龄都会加1,这里4位来表示分代年龄最大值为15,这也就是为什么对象的年龄超过15后会被移到老年代的原因。在启动时可以通过添加参数来改变年龄阈值:

    -XX:MaxTenuringThreshold
    

    当设置的阈值超过15时,启动时会报错:

    3.2 Klass Pointer 类型指针

    Klass Pointer是一个指向方法区中Class信息的指针,虚拟机通过这个指针确定该对象属于哪个类的实例。在64位的JVM中,支持指针压缩功能,根据是否开启指针压缩,Klass Pointer占用的大小将会不同:

    • 未开启指针压缩时,类型指针占用8B (64bit)

    • 开启指针压缩情况下,类型指针占用4B (32bit)

    jdk6之后的版本中,指针压缩是被默认开启的,可通过启动参数开启或关闭该功能:

    #开启指针压缩:
    -XX:+UseCompressedOops
    #关闭指针压缩:
    -XX:-UseCompressedOops
    

    还是以刚才的User类为例,关闭指针压缩后再次查看对象的内存布局:

    对象大小虽然还是16字节,但是组成发生了改变,8字节标记字加8字节类型指针,已经能满足对齐条件,因此不需要填充。

    8B (mark word) + 8B (klass pointer) + 0B (instance data) + 0B (padding)
    

    3.2.1 指针压缩原理

    在了解了指针压缩的作用后,我们来看一下指针压缩是如何实现的。首先在不开启指针压缩的情况下,一个对象的内存地址使用64位表示,这时能描述的内存地址范围是:

    0 ~ 2^64-1
    

    在开启指针压缩后,使用4个字节也就是32位,可以表示2^32 个内存地址,如果这个地址是真实地址的话,由于CPU寻址的最小单位是Byte,那么就是4GB内存。这对于我们来说是远远不够的,但是之前我们说过,java中对象默认使用了8字节对齐,也就是说1个对象占用的空间必须是8字节的整数倍,这样就创造了一个条件,使jvm在定位一个对象时不需要使用真正的内存地址,而是定位到由java进行了8字节映射后的地址(可以说是一个映射地址的编号)。

    映射过程也非常简单,由于使用了8字节对齐后每个对象的地址偏移量后3位必定为0,所以在存储的时候可以将后3位0抹除(转化为bit是抹除了最后24位),在此基础上再去掉最高位,就完成了指针从8字节到4字节的压缩。而在实际使用时,在压缩后的指针后加3位0,就能够实现向真实地址的映射。

    完成压缩后,现在指针的32位中的每一个bit,都可以代表8个字节,这样就相当于使原有的内存地址得到了8倍的扩容。所以在8字节对齐的情况下,32位最大能表示2^32*8=32GB内存,内存地址范围是:

    0 ~ (2^32-1)*8
    

    由于能够表示的最大内存是32GB,所以如果配置的最大的堆内存超过这个数值时,那么指针压缩将会失效。配置jvm启动参数:

    -Xmx32g
    

    查看对象内存布局:

    此时,指针压缩失效,指针长度恢复到8字节。那么如果业务场景内存超过32GB怎么办呢,可以通过修改默认对齐长度进行再次扩展,我们将对齐长度修改为16字节:

    -XX:ObjectAlignmentInBytes=16 -Xmx32g
    

    可以看到指针压缩后占4字节,同时对象向16字节进行了填充对齐,按照上面的计算,这时配置最大堆内存为64GB时指针压缩才会失效。

    对指针压缩做一下简单总结:

    • 通过指针压缩,利用对齐填充的特性,通过映射方式达到了内存地址扩展的效果

    • 指针压缩能够节省内存空间,同时提高了程序的寻址效率

    • 堆内存设置时最好不要超过32GB,这时指针压缩将会失效,造成空间的浪费

    • 此外,指针压缩不仅可以作用于对象头的类型指针,还可以作用于引用类型的字段指针,以及引用类型数组指针

    3.3 数组长度

    如果当对象是一个数组对象时,那么在对象头中有一个保存数组长度的空间,占用4字节(32bit)空间。通过下面代码进行测试:

    public static void main(String[] args) {
        User[] user=new User[2];
        //查看对象的内存布局
        System.out.println(ClassLayout.parseInstance(user).toPrintable());
    }
    

    运行代码,结果如下:

    内存结构从上到下分别为:

    • 8字节mark word

    • 4字节klass pointer

    • 4字节数组长度,值为2,表示数组中有两个元素

    • 开启指针压缩后每个引用类型占4字节,数组中两个元素共占8字节

    需要注意的是,在未开启指针压缩的情况下,在数组长度后会有一段对齐填充字节:

    通过计算:

    8B (mark word) + 8B (klass pointer) + 4B (array length) + 16B (instance data)=36B
    

    需要向8字节进行对齐,这里选择将对齐的4字节添加在了数组长度和实例数据之间。

    4、实例数据

    实例数据(Instance Data)保存的是对象真正存储的有效信息,保存了代码中定义的各种数据类型的字段内容,并且如果有继承关系存在,子类还会包含从父类继承过来的字段。

    • 基本数据类型:

    TypeBytes
    byte,boolean1
    char,short2
    int,float4
    long,double8
    • 引用数据类型:

    开启指针压缩情况下占8字节,开启指针压缩后占4字节。

    4.1 字段重排序

    给User类添加基本数据类型的属性字段:

    public class User {
        int id,age,weight;
        byte sex;
        long phone;
        char local;
    }
    

    查看内存布局:

    可以看到,在内存中,属性的排列顺序与在类中定义的顺序不同,这是因为jvm会采用字段重排序技术,对原始类型进行重新排序,以达到内存对齐的目的。具体规则遵循如下:

    • 按照数据类型的长度大小,从大到小排列

    • 具有相同长度的字段,会被分配在相邻位置

    • 如果一个字段的长度是L个字节,那么这个字段的偏移量(OFFSET)需要对齐至nL(n为整数)

    上面的前两条规则相对容易理解,这里通过举例对第3条进行解释:

    因为long类型占8字节,所以它的偏移量必定是8n,再加上前面对象头占12字节,所以long类型变量的最小偏移量是16。通过打印对象内存布局可以发现,当对象头不是8字节的整数倍时(只存在8n+4字节情况),会按从大到小的顺序,使用4、2、1字节长度的属性进行补位。为了和对齐填充进行区分,可以称其为前置补位,如果在补位后仍然不满足8字节整数倍,会进行对齐填充。在存在前置补位的情况下,字段的排序会打破上面的第一条规则。

    因此在上面的内存布局中,先使用4字节的int进行前置补位,再按第一条规则从大到小顺序进行排列。如果我们删除3个int类型的字段,再查看内存布局:

    charbyte类型的变量被提到前面进行前置补位,并在long类型前进行了1字节的对齐填充。

    4.2 拥有父类情况

    • 当一个类拥有父类时,整体遵循在父类中定义的变量出现在子类中定义的变量之前的原则

    public class A {
        int i1,i2;
        long l1,l2;
        char c1,c2;
    }
    public class B extends A{
        boolean b1;
        double d1,d2;
    }
    

    查看内存结构:

    • 如果父类需要后置补位的情况,可能会将子类中类型长度较短的变量提前,但是整体还是遵循子类在父类之后的原则

    public class A {
        int i1,i2;
        long l1;
    }
    public class B extends A {
        int i1,i2;
        long l1;
    }
    

    查看内存结构:

    可以看到,子类中较短长度的变量被提前到父类后进行了后置补位。

    • 父类的前置对齐填充会被子类继承

    public class A {
        long l;
    }
    public class B extends A{
        long l2;
        int i1;
    }
    

    查看内存结构:

    当B类没有继承A类时,正好满足8字节对齐,不需要进行对齐填充。当B类继承A类后,会继承A类的前置补位填充,因此在B类的末尾也需要对齐填充。

    4.3 引用数据类型

    在上面的例子中,仅探讨了基本数据类型的排序情况,那么如果存在引用数据类型时,排序情况是怎样的呢?在User类中添加引用类型:

    public class User {
         int id;
         String firstName;
         String lastName;
         int age;
    }
    

    查看内存布局:

    可以看到默认情况下,基本数据类型的变量排在引用数据类型前。这个顺序可以在jvm启动参数中进行修改:

    -XX:FieldsAllocationStyle=0
    

    重新运行,可以看到引用数据类型的排列顺序被放在了前面:

    FieldsAllocationStyle的不同取值简要说明:

    • 0:先放入普通对象的引用指针,再放入基本数据类型变量

    • 1:默认情况,表示先放入基本数据类型变量,再放入普通对象的引用指针

    4.4 静态变量

    在上面的基础上,在类中加入静态变量:

    public class User {
         int id;
         static byte local;
    }
    

    查看内存布局:

    通过结果可以看到,静态变量并不在对象的内存布局中,它的大小是不计算在对象中的,因为静态变量属于类而不是属于某一个对象的。

    5、对齐填充字节

    Hotspot的自动内存管理系统中,要求对象的起始地址必须是8字节的整数倍,也就是说对象的大小必须满足8字节的整数倍。因此如果实例数据没有对齐,那么需要进行对齐补全空缺,补全的bit位仅起占位符作用,不具有特殊含义。

    在前面的例子中,我们已经对对齐填充有了充分的认识,下面再做一些补充:

    • 在开启指针压缩的情况下,如果类中有long/double类型的变量时,会在对象头和实例数据间形成间隙(gap),为了节省空间,会默认把较短长度的变量放在前边,这一功能可以通过jvm参数进行开启或关闭:

    # 开启
    -XX:+CompactFields
    # 关闭
    -XX:-CompactFields
    

    测试关闭情况,可以看到较短长度的变量没有前移填充:

    • 在前面指针压缩中,我们提到了可以改变对齐宽度,这也是通过修改下面的jvm参数配置实现的:

    -XX:ObjectAlignmentInBytes
    

    默认情况下对齐宽度为8,这个值可以修改为2~256以内2的整数幂,一般情况下都以8字节对齐或16字节对齐。测试修改为16字节对齐:

    上面的例子中,在调整为16字节对齐的情况下,最后一行的属性字段只占了6字节,因此会添加10字节进行对齐填充。当然普通情况下不建议修改对齐长度参数,如果对齐宽度过长,可能会导致内存空间的浪费。

    6、总结

    本文通过使用jol 对java对象的结构进行调试,学习了对象内存布局的基本知识。通过学习,能够帮助我们:

    • 掌握对象内存布局,基于此基础进行jvm参数调优

    • 了解对象头在synchronize 的锁升级过程中的作用

    • 熟悉 jvm 中对象的寻址过程

    • 通过计算对象大小,可以在评估业务量的基础上在项目上线前预估需要使用多少内存,防止服务器频繁gc

    想了解更多相关学习资料请点赞收藏+评论转发+关注我之后点进我的主页右上角私信(555)即可领取免费资料

     

    原文出处:图文详解Java对象内存布局

    展开全文
  • jvm jvm内存布局
  • new Object()到底占用几个字节

    首先我们看看JAVA内存布局

    一:java 对象布局

    在Hotspot虚拟机中,对象在内存中存储的布局可以分为三块区域:
    对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

    对象头(Object Header):

    32位系统对象头大小为8个字节(864bit)
    64位系统对象头大小为16个字节 (128bit)

    在这里插入图片描述
    (图片都来自图灵学院)

    • mark word

    用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit

    32位JVM下的对象结构描述
    在这里插入图片描述
    64位JVM下的对象结构描述
    在这里插入图片描述

    • Klass Pointer

    对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 32位4字节,64位开启指针压缩或最大堆内存<32g时4字节,否则8字节。jdk1.8默认开启指针压缩后为4字节,当在JVM参数中关闭指针压缩(-XX:-UseCompressedOops)后,长度为8字节。

    • 数组长度 (只有数组对象有)

    如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度。 4字节

    对于个空对象:
    在这里插入图片描述

    实例数据(Instance Data:

    对象中定义的成员属性

    对齐填充(Padding):

    对齐填充并不是必然存在的,也没有特定的含义,仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。对象字节数/8的倍数,如果不足的话 实现填充。例如:一个对象21个字节,则需要补充3个字节。

    二.代码验证

    引入JOL(JAVA OBJECT LAYOUT)包,使用此工具可以查看一个java对象的内存布局及占用多少字节

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

    1. 案例一

    
        /**
         * 默认开启指针压缩
         */
        @Test
        public void test() {
            Object obj = new Object();
            //查看对象内部信息
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    
    

    打印信息:
    在这里插入图片描述
    名称解释:

    OFFSET:偏移地址,单位字节;
    SIZE:占用的内存大小,单位为字节(Byte);
    TYPE DESCRIPTION:类型描述,其中object header为对象头;
    VALUE:对应内存中当前存储的值,二进制32位;


    注意:在jdk1.8中,是默认开启指针压缩

    • 行(offset为0)及第行(offset为4)的object header为mark word,占8个字节

    • 行(offset为8)object header是kclass point,占4个字节
      (开启指针压缩,占4个字节,不开启指针压缩,占用8字节)

    • 行(offset为12)对齐填充
      对象的大小必须是8byte的整数倍, 前面占用了12byte,要凑成8个整数呗,所以需要对齐填充(padding)4byte

    2.案例二:

      /**
         *  不开启指针压缩 (-XX:-UseCompressedOops)
         */
        @Test
        public void test2() {
            Object obj = new Object();
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
    

    在这里插入图片描述
    手动关闭指针压缩情况下

    • 行(offset为0)和第行(offset为4)的object header为mark word,占8个字节
    • 行(offset为8)和第行(offset为12)的object header是kclass point,占8个字节
      8 +8 =16,所以不需要对齐填充(padding)了

    3.案例三

    
    	/**
    	  对象含有属性的情况下
    	*/
        @Test
        public void test3() {
            Object obj = new TestObject();
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }
    
    class TestObject {
        private long p;
    }
    
    

    在这里插入图片描述

    • 行(offset为0)和第行(offset为4)的object header为mark word,占8个字节

    • 行(offset为8)object header是kclass point,占4个字节

    • 行(offset为16)为实例大小
      long类型占8个字节

    • 行(offset为12)对齐填充
      对齐填充(padding)4byte(这里是间隙填充alignment/padding gap)//@TODO

    //复习一下
    byte b; 		 1字节  	  默认是0
    short s; 		 2字节    默认是0
    int i; 			 4字节 	 默认是0
    long l; 		 8字节  	 默认是0L
    char c; 		 2字节  	 默认是0对应的字符 `u0000`
    float f; 		 4字节  	 默认是0.0f
    double d;		 8字节 	 默认是0.0d
    boolean bool;    1字节 	 默认是false
    

    4.案例四:

        /**
         * 空数组对象
         */
        @Test
        public void test5() {
            int[] arr = new int[0];
            System.out.println(ClassLayout.parseInstance(arr).toPrintable());
    
        }
    
    

    在这里插入图片描述

    • 行(offset为0)和第行(offset为4)的object header为mark word,占8个字节

    • 行(offset为8)object header为kclass point,占4个字节

    • 行(offset为12)为数组长度,占4个字节

    其他情况:

        /**
        如果是引用类型
        */
        @Test
        public void test7() {
            Object obj = new TestObject7();
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    
        }
        class TestObject7 {
        private Integer integer;
    }
    
    

    在这里插入图片描述

        @Test
        public void test6() {
            Object obj = new TestObject6();
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }
        class TestObject6 {
        private long p;
        private int i;
        private boolean b2;
    }
    
    

    在这里插入图片描述

    参考:
    https://mp.weixin.qq.com/s/NfAM8WMfemrozn38QC7STA
    https://mp.weixin.qq.com/s/yft-C_INozNL2dxx0DVo9w

    展开全文
  • java 对象内存布局

    2021-05-16 17:45:58
    在了解之前,我们先看看一个面试过程 在面试的时候面试官经常会出现一道题:“Object o = new Object() 占用了多少内存?... java对象内存布局主要的作用用来表示一个对象在计算机中占用了多少内存;内存布局主...
  • java对象在堆内存中的存储布局可以分为三个部分:对象头(Header)、实力数据(Instance Data)、对齐填充(Padding)。
  • Java对象内存布局和对象头

    千次阅读 2022-06-26 15:29:03
    1、对象内存布局 在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)2、对象在堆内存中的存储布局Mark Word:对象标记 Class ...
  • Java对象内存布局概述

    千次阅读 多人点赞 2019-09-22 23:17:42
    以HotSpot虚拟机为例,对象内存中可以分为三块区域:对象头、实例数据和对齐填充。其中,对象头包含Mark Word和类型指针,关于对象头的内容,在gitchat中对其实现和原理都已经结合openjdk源码进行了详细的说明,其...
  • java对象内存布局

    千次阅读 2018-09-21 17:46:04
    我们用如下代码来说一下java对象内存布局。 class A { long l; int i; } class B extends A { long l; int i; }   如图所示展示的是new B()在堆中的内存模型:  JVM中,每个对象都有一个对象...
  • Java 对象内存布局

    2018-02-13 09:21:48
    在HotSpot虚拟机中,对象内存中存储的布局可以分为三块区域:对象头(Header)、 实例数据(Instance Data)、对齐填充(Padding)。 对象头 HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时...
  • Java对象内存布局与压缩指针原理

    千次阅读 2020-06-20 19:24:05
    Java对象内存中的结构 一个Java对象,依据Hotspot的实现来讲,分为三块区域:对象头,实例数据,对齐填充块,如下图 首先来认识下对象头 对象头(Header) 对象头由两部分组成:一部分是Markword,另一部分是...
  • 介绍了heap dump和thread dump,以及详细介绍dump工具Memory Analyzer的使用,最后讲解了Java对象内存布局
  • 10Java对象内存布局1

    2022-08-04 14:07:06
    // Foo 类构造器会调用其父类 Object 的构造器1 invokespecial java.lang.Object() [8]然后,子类的构造器需要调用
  • JAVA对象内存布局

    千次阅读 2014-07-12 13:39:58
    1、java对象内存布局 在hotSpot虚拟机中,对象在内存中的
  • Java对象内存布局与访问定位

    千次阅读 2018-09-29 14:44:32
    Java对象布局 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Java对象由三部分构成:对象头、实例数据、对齐补充。 (1)对象头 &nbsp;&nbsp;&nbsp...
  • 对象内存布局图: 对象头中 Mark Word占8字节,Class Pointer占4字节(JVM默认开启-XX:+UseCompressedOops选项),new Object不存在属性,因instance data不占字节, JVM保证任意对象在内存中占用的字节数都是8...
  • JAVA对象内存布局 以及对象定位 1、Java对象内存布局  在hotSpot虚拟机中,对象在内存中的布局可以分成对象头、实例数据、对齐填充三部分。  对象头:对象头包含两部分,第一部分主要包括对象自身的运行行元数据...
  • 最近在学习java对象内存布局方面的一些知识,主要是想知道一个java对象到底占用多少内存空间,以及java对象在内存中到底是什么样子的。c/c++中的sizeof运算符能够方便地告诉我们一个变量占用的内存空间,但是在java...
  • Java对象内存布局

    千次阅读 2015-08-14 12:03:48
    本文包括对简单java对象内存布局的介绍,介绍一个对象的大小,成员在内存中的位置等,不包括继承情况下布局的介绍
  • 主要介绍了深入理解JVM之Java对象的创建、内存布局、访问定位,结合实例形式详细分析了Java对象的创建、内存布局、访问定位相关概念、原理、操作技巧与注意事项,需要的朋友可以参考下
  • 或者如何打印java 对象内存布局呢? 下面介绍可以使用这个类 首先引入这个包 <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <...
  • 包含的主要过程包括了类加载检查、对象分配内存、并发处理、内存空间初始化、对象设置、执行ini方法等。主要流程如下:1. 类加载检查JVM遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的...
  • Java对象内存分配

    千次阅读 2022-03-15 09:33:25
    文章目录Java对象的内存分配对象的创建流程类加载检查对象内存分配对象的内存布局对象头区域实例数据区域填充对齐区域对象的访问定位方式句柄访问直接指针访问 Java对象的内存分配 对象的创建流程 虚拟机收到new...
  • 计算对象大小需要了解java对象内存布局。在HotSpot 虚拟机中,对象在内存中布局分为三块区域,对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。 对象头 对象头主要包括Mark Word,对象指针,...
  • Java中的每一个对象都可以作为锁,也就是我们常说的“Java中每个对象都持有一把锁”,这是synchronized实现同步的基础。 synchronized实现同步具体表现为以下三种形式: 1. 对于普通同步方法,锁是当前实例对象 2. ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 80,277
精华内容 32,110
关键字:

java对象内存布局