精华内容
下载资源
问答
  • Java虚拟机遇到一条字节码new指令时,检查指令的参数能否在常量池中定位一个符号引用。 检查类是否被加载 检查这个符号引用代表的类是否已被加载、解析和初始化过。 类加载过程 确定所加载类占用内存大小,...

    1.虚拟机对象创建

    ​ 语言层面上,创建对象通常(例外:复制、反序列化)仅仅是一个new关键字而已,本文所探讨的虚拟机对象创建不包含数组和Class对象等,就对于普通对象而言。

    1. 常量池中定位符号引用

      • Java虚拟机遇到一条字节码new指令时,检查指令的参数能否在常量池中定位一个符号引用。
    2. 检查类是否被加载

      • 检查这个符号引用代表的类是否已被加载、解析和初始化过。
    3. 类加载过程

      • 确定所加载类占用内存大小,并划分空间进行存储
      • 划分空间两种方式
        • 指针碰撞,堆内存规整
          • 所代表的垃圾收集器有Serial、ParNew,用算法标记-整理算法
          • 可能导致线程不安全问题,可看下方tip1:指针碰撞所带来的线程安全问题。
        • 空闲列表
          • 所代表的垃圾收集器CMS,采用标记清除算法
      • 对象设置
        • 虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码对象的GC分代年龄等信息。。这些信息存放在对象的对象头(Tip2:对象都存储内容)之中。

      上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了。但是从Java程序的视角看来,对象创建才刚刚开始——构造函数,即Class文件中的()方法还没有执行,所有的字段都为默认的零值。梳理完虚拟机对象创建过程后,在来看看对象在堆内存的存储布局.

    2.对象的内存布局

    在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

    2.1对象头

    HotSpot虚拟机对象的对象头部分包括两类信息:

    1. 第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称它为“Mark Word”。
    2. 第二类类型指针

    2.2实例数据

    ​ 实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在Java源码中定义顺序的影响。

    ​ HotSpot虚拟机默认的分配顺序为longs/doublesintsshorts/charsbytes/booleans、oops。

    ​ 从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。

    ​ 对象的第三部分是对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。

    2.3对齐填充

    ​ 由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

    ​ 总的一句话来说,“数据项仅仅能存储在地址是数据项大小的整数倍的内存位置上(分别为偶地址、被4整除的地址、被8整除的地址)”比如int类型占用4个字节,地址仅仅能在0,4,8等位置上。

    ​ 要讲好对齐填充,其实还是得去了解c++实现。

    例1:

    #include <stdio.h>
    struct xx{
        char b;
        int a;
        int c;
        char d;
    };   //总体结构用了16个字节
    
    struct xx{
            char b; 
            char d;
            int a;          
            int c;                  
    };	//总体结构占用12个字节
    

    只说结论,过程可以将对应得字段地址打印出来。

    会发现b之后填充3字节,d之后也会填充3字节,这儿就体现了对齐填充,平时代码,分类要做好,对齐填充也要注意。

    Tip1:指针碰撞所带来的线程安全问题。

    ​ 对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种可选方案:

    1. 一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;
    2. 另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local AllocationBuffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步处理。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

    Tip2:对象头存储内容

    对象头又称"Mark Word"。定义了一个有着动态数据的数据结构例如在32位的HotSpot虚拟机中,如对象未被同步锁锁定的状态下,Mark Word的32个比特存储空间中的25个比特用于存储对象哈希码4个比特用于存储对象分代年龄2个比特用于存储锁标志位1个比特固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如表2-1所示。

    在这里插入图片描述

    对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。

    展开全文
  • 对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。 对象头(Header) 对象头分为两类信息:一类是用于存储对象自身的运行时数据,一类是类型指针。 第一...

    对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

    对象头(Header)

    对象头分为两类信息:一类是用于存储对象自身的运行时数据,一类是类型指针。

    1. 第一部分是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。 这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称它为"Mark Word"

    2. 第二部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。 并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。此外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据。因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息u梯段出数组的大小。

    实例数据(Instance Data)

    实例数据:示例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。 这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle 参数)和字段在Java源码中定义顺序的影响。

    HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers, OOPs),从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。 如果虚拟机的+XX:CompactFields参数值为true(默认为true),那子类之中较窄的变量也允许插入父类变量的空隙中,以节省出一点点空间。

    对齐填充(Padding)

    对齐填充:对齐填充并不是必然存在的,也没有特殊的含义,它仅仅起着占位符的作用。 由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。 对象头部分已经被精心设计成正好是8字节的倍数(1倍或2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

    展开全文
  • 上一篇文章我们讲解了JVM对象内存布局的第一部分对象头,今天我们继续来讲讲剩下的两部分实例数据(Instance Data) 、对齐填充(Padding)。 实例数据与对齐填充 这两部分我们放在一起将,先来看一下概念(深入理解...

    引言

    上一篇文章我们讲解了JVM对象内存布局的第一部分对象头,今天我们继续来讲讲剩下的两部分实例数据(Instance Data) 、对齐填充(Padding)。

    实例数据与对齐填充

    这两部分我们放在一起将,先来看一下概念(深入理解JAVA虚拟机 第2.3.2节):

    实例数据(Instance Data):

    实例数据部分是对象真正存储的有效信息,也既是我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的都需要记录下来。 这部分的存储顺序会受到虚拟机分配策略参数FieldsAllocationStyle和字段在Java源码中定义顺序的影响。HotSpot虚拟机 默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果 CompactFields参数值为true(默认为true),那子类之中较窄的变量也可能会插入到父类变量的空隙之中。

    对齐填充(Padding):

    对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。对象头正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

    上面其实说的也很明白,实例数据就是我们所赋予给对象的那些属性信息,这部分内容是紧挨着对象头(Header)存储的,这没啥好说的;所以我们今天要研究的就是实例数据中的数据是否是按照我们在类中定义的那样排序的,每部分数据所占用的大小是怎样的,虚拟机分配策略又是什么以及怎么样根据定义的类来估算一个对象的大小。

    代码验证

    咱们直接用代码配合启动参数的修改再结合输出结果来解答上述问题。

    先把老朋友Person类抬上来,接下来因为要频繁修改属性定义,写Getter和Setter太麻烦了,我就直接用了lombok。

    		<!-- 除了pom引入依赖外还需要安装插件,请自行百度 -->
    		<dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>RELEASE</version>
            </dependency>
    
    package com.cjf.test;
    
    import lombok.Data;
    import org.openjdk.jol.info.ClassLayout;
    
    /**
     * @author Chenjf
     * @date 2020/8/6
     **/
    @Data
    public class Person {
        private Person children;
        private String name;
        private Integer age;
        private String address;
        private Boolean man;
    
        
        public static void printf(Person p) {
            // 查看对象的整体结构信息
            //JOL工具类
            System.out.println(ClassLayout.parseInstance(p).toPrintable());
        }
    
    }
    
    

    先直接输出一波:

        public static void main(String[] args) {
            Person person = new Person();
            //输出对象内存地址
            System.out.println(GraphLayout.parseInstance(person).toPrintable());
            //打印对象布局信息
            printf(person);
        }
    

    输出结果:
    在这里插入图片描述

    从图中可以看到,偏移量为12,大小为4字节的位置存放着Person类中children属性的指针。

    引申问题

    为什么是指针而不是具体的数据?

    我说一句你就懂了,如果另一个Person的children和这个对象的children是相同的呢,如果存具体数据的话是不是就得存两份,完全没意义啊。所以这里肯定存的是指针,只要能定位到具体的数据就可以了,这样无论哪个对象引用了另一个对象,该对象的实例数据中只要保存指向那个对象的指针即可。另外我们可以看到,实例数据中的Size清一色的都是4,在64位系统下光一个对象头就占12字节了(开启了指针压缩),所以这里必然存的是指向另一个对象的指针。

    那为什么Size是4不是5不是6不是其它的呢?

    刚才说了实例数据中children属性存的是指向这个children对象的指针;那不妨大胆猜测一下,这个指针究竟是什么?我本人猜测存的就是内存地址,但不是绝对地址是相对地址,具体如何定位的我们也无需去关心,JVM拿到这串地址之后,通过简单运算之后可以定位到就行了。当然我只是猜的,因为这部分数据JOL它没输出给我,我从hsdb中也看不到。既然我猜测是地址,那么就顺着这个思路去猜。

    32位处理器,计算机中的位数指的是CPU一次能处理的最大位数。32位计算机的CPU一次最多能处理32位数据,例如它的EAX寄存器就是32位的,当然32位计算机通常也可以处理16位和8位数据。在Intel由16位的286升级到386的时候,为了和16位系统兼容,它先推出的是386SX,这种CPU内部预算为32位,外部数据传输为16位。直到386DX以后,所有的CPU在内部和外部都是32位的了。

    在计算机中,“位(bit)”和"字节(Byte)"、KB、MB以及TB的关系是:8位等于一字节,即8bit=1Byte,1KB=1024Byte(字节)=8*1024bit,1MB=1024KB,1GB=1024MB,1TB=1024GB 。

    32位处理器每次处理 4Byte(32bit),同理,64位处理器每次处理 8Byte(64bit) 。

    32位处理器每次能处理4字节的单位,所以这个指针存的就是4字节的内存地址(相对地址),这就是为什么Size是4而不是其它的原因。

    可是我的机器是64位的啊,为什么是4而不是8?

    还记得之前总提到的指针压缩吗?因为在JDK1.6之后,JVM在64位操作系统中默认开启了指针压缩技术(堆内存小于4G时无需开启,直接舍弃高位用地位,堆内存大于32G时指针压缩无效)。那么我们把指针压缩关掉再测试一下,实例数据中的Size果然都变成了8。

    -XX:-UseCompressedOops

    输出结果如下:

    在这里插入图片描述

    为什么要开启指针压缩呢?

    以下内容摘自 JVM压缩指针

    在堆中,32位的对象引用(指针)占4个字节,而64位的对象引用占8个字节。也就是说,64位的对象引用大小是32位的2倍。64位JVM在支持更大堆的同时,由于对象引用变大却带来了性能问题。

    • 增加了GC开销:64位对象引用需要占用更多的堆空间,留给其他数据的空间将会减少,从而加快了GC的发生,更频繁的进行GC。

    • 降低CPU缓存命中率:64位对象引用增大了,CPU能缓存的oop将会更少,从而降低了CPU缓存的效率。

    为了能够保持32位的性能,oop必须保留32位。那么,如何用32位oop来引用更大的堆内存呢?

    答案是——压缩指针(CompressedOops)。

    什么是oop

    OOP = “ordinary object pointer” 普通对象指针。 启用CompressOops后,会压缩的对象:

    1. 每个Class的属性指针(静态成员变量)
        2. 每个对象的属性指针
        3. 普通对象数组的每个元素指针

    当然,压缩也不是万能的,针对一些特殊类型的指针,JVM是不会优化的。 比如指向 PermGen的Class 对象指针,本地变量,堆栈元素,入参,返回值,NULL指针不会被压缩。

    那对象的引用为4字节,是不是代表最大内存也被限制在了(2^32) 4G呢?

    答案不是。

    当采用4字节表示引用时, 直观来看是表示4G bytes大小的空间, 但是, 由于对象分配时是8字节对齐的(后面会讲到,即使引用为4字节也是按8字节对齐,所以就有了对齐填充), 也就是对象指针的低3bit是0, 因此可以把这3bit压缩掉, 实际32bit的可以表示4G * 8 bytes = 32G bytes的内存空间, 对于大部分服务来说足够了。因此堆内存小于32G时, 指针压缩默认开,超过32G,指针压缩就不生效了。

    回到我们的代码验证,现在已经知道了如果是对象类型,那么实例数据中存放的即为这个对象的引用;那么如果是基本数据类型存放的是什么呢?是具体的数据,一起来验证一下。

    修改Person类的定义,如下:

    package com.cjf.test;
    
    import lombok.Data;
    import org.openjdk.jol.info.ClassLayout;
    import org.openjdk.jol.info.GraphLayout;
    import org.openjdk.jol.vm.VM;
    
    /**
     * @author Chenjf
     * @date 2020/8/6
     **/
    @Data
    public class Person {
        private Person children;
        private String name;
        private long height;
        private int age;
        private String address;
        private boolean man;
    
    
        public static void printf(Person p) {
            // 查看对象的整体结构信息
            //JOL工具类
            System.out.println(ClassLayout.parseInstance(p).toPrintable());
        }
    
    }
    
    

    测试代码:

     public static void main(String[] args) {
            Person person = new Person();
            person.setName("小陈");
            person.setAge(18);
            Person children = new Person();
            children.setName("小小陈");
            children.setAge(1);
            person.setChildren(children);
    
            System.out.println(Integer.toHexString((int) VM.current().addressOf(person)));
            //输出对象内存地址
            System.out.println(GraphLayout.parseInstance(person).toPrintable());
            //打印对象布局信息
            printf(person);
        }
    

    输出结果如下:

    在这里插入图片描述

    首先后面的value不再只显示一个(Object),而是直接显示了具体的值;其次,我们可以发现Size的大小也不再是清一色的4了。int为4,long为8,boolean为1,是不是很眼熟。

    基本数据类型有8种:byte、short、int、long、float、double、boolean、char

    计算机的基本单位:bit .  一个bit代表一个0或1   1个字节是8个bit

    byte:1byte = 8bit short:2byte int:4byte  long:8byte

    ​ float:4byte double:8byte boolean:1byte char:2byte

    由此可以得知,实例数据中的基本数据类型存放的是具体的值。因此,int 所能表示的范围为-2^31 ~ 2^31-1,即 -2147483648 ~ 2147483647

    //JAVA中Intger类的定义
    @Native public static final int MIN_VALUE = 0x80000000;
    @Native public static final int MAX_VALUE = 0x7fffffff;
    

    然后我们发现在属性man(boolean类型)的下面,还有一串(alignment/padding gap) 大小为3字节,这又是啥嘞?

    字面含义是(对齐/空隙填充),没错这个就是对齐填充。还记得我们刚才说的JVM管理内存都是以8字节对齐的吗(64位是这样的,我没有32位机器,无法验证32位下是按照8字节对齐还是4字节对齐,其实按8字节对齐就一定是按4字节对齐,因为8永远是4的整数倍),即对象的大小永远都是8字节的整数倍。

    我们知道64位系统CPU每次能处理8字节的数且只能以
    0x00000000 - 0x00000007,0x00000008-0x0000000f这样访问内存地址,不能0x00000002 - 0x00000009这样访问(为什么?因为硬件不允许,否则就不需要对齐填充了)。
    那么如果没有对齐填充就可能会存在数据跨内存地址区域存储的情况。

    举个栗子:

    比如现在有4个数据,boolean,int,char,long,内存起始地址为0x00(简写)。

    在没有对齐填充的情况下,内存地址存放情况如下:

    在这里插入图片描述

    因为处理器只能0x00-0x07,0x08-0x0F这样读取数据,所以当我们想获取这个long型的数据时,处理器必须要读两次内存,第一次(0x00-0x07),第二次(0x08-0x0F),然后将两次的结果才能获得真正的数值。

    那么在有对齐填充的情况下,内存地址存放情况是这样的:
    在这里插入图片描述
    现在处理器只需要直接一次读取(0x08-0x0F)的内存地址就可以获得我们想要的数据了。

    对齐填充存在的意义就是为了提高CPU访问数据的效率,这是一种以空间换时间的做法;正如我们所见,虽然访问效率提高了(减少了内存访问次数),但是在0x07处产生了1bit的空间浪费。

    那么如何才能不浪费掉这1字节的地址空间呢?

    试想一下,如果此时又有一个1字节的数据存到内存中,是不是可以把这1字节的数据直接插到0x07的地方,这样就不需要填充也能实现8字节对齐了呢?没错,JVM就是这样做的,因此JVM在为对象分配内存时,对象中的实例数据的排序规则并不是完全按照我们在类中的所定义的顺序来排序的。

    回到实例数据的概念:

    实例数据部分是对象真正存储的有效信息,也既是我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的都需要记录下来。 这部分的存储顺序会受到虚拟机分配策略参数FieldsAllocationStyle和字段在Java源码中定义顺序的影响。HotSpot虚拟机 默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果 CompactFields参数值为true(默认为true),那子类之中较窄的变量也可能会插入到父类变量的空隙之中。

    虚拟机分配策略的存在能尽可能的帮助我们在对其填充的情况下减少内存地址的空间浪费,下面就展开来讲讲虚拟机分配策略吧。

    用Clion打开HotSpot源码,直接搜索一下FieldsAllocationStyle,然后就可以在globals.hpp的第1250行看到下面这个定义(HotSpot在线源码

      product(intx, FieldsAllocationStyle, 1,                                   \
              "0 - type based with oops first, 1 - with oops last, "            \
              "2 - oops in super and sub classes are together")                 \
    

    可以看到这是一个int类型的参数,默认值为1,取值范围为0,1,2代表着三种分配策略。

    • 取值为0时,引用在原始类型前面, 然后依次是longs/doubles、ints、shorts/chars、bytes/booleans;即基本类型->填充字段(可以没有)->引用类型

    • 取值为1时,引用在原始类型后面,即longs/doubles、ints、shorts/chars、bytes/booleans 然后是引用类型;即引用类型->基本类型->填充字段(可以没有)

    • 取值为2时,父类中的引用类型与子类中的引用类型放在一起,此时父类采用策略0,子类采用策略1。

    取值0和取值1都是将基本数据类型按照从大到小的方式排序,这样可以降低空间开销。而取值3 将父类和子类的引用放在一起,这样可以增加 GC 效率,试想在GC 扫描引用时,由于父类和子类的引用连续,可能只需要扫描一个内存行即可,若父类和子类的引用不连续,则需要扫描多个内存行;另外连续的内存引用还可减少 OopMap 的个数,从而达到提高 GC 效率的目的。

    再回头看看我们刚才的测试结果,排序却是按照age(int)、height(long)、man(boolean)然后是引用类型;基本类型的排序不符合longs/doubles、ints、shorts/chars、bytes/booleans的规则啊。原因很简单,因为对象头占了12字节,还记得我们刚刚说的8字节对齐吗,这里正好可以插入一个4字节的数据,如果long类型排在前面的话,这4字节是不是就浪费了呢,所以JVM就帮我们把age(int)放到了long类型之前(可以通过-XX:+/-CompactFields进行控制,默认开启);

    我们关闭指针压缩,来验证一下:

    在这里插入图片描述

    可以看到结果是long类型按照规则确实排在了int前面。

    现在我们再简单验证一下策略0:

    //启动参数 开启指针压缩(默认开启)   指定分配策略为0
    -XX:+UseCompressedOops -XX:FieldsAllocationStyle=0
    

    测试代码不变:

      public static void main(String[] args) {
            Person person = new Person();
            person.setName("小陈");
            person.setAge(18);
            Person children = new Person();
            children.setName("小小陈");
            children.setAge(1);
            person.setChildren(children);
    
            System.out.println(Integer.toHexString((int) VM.current().addressOf(person)));
            //输出对象内存地址
            System.out.println(GraphLayout.parseInstance(person).toPrintable());
            //打印对象布局信息
            printf(person);
    
        }
    

    输出结果如下:

    在这里插入图片描述

    符合我们的预期结果,引用类型->基本类型->填充字段。

    最后来看一看策略2,先改动一下我们的Person类让它继承Biology类。

    public class Biology {
        protected String type;
        protected int id;
    }
    
    @Data
    public class Person extends Biology{
        private Person children;
        private String name;
        private long height;
        private int age;
        private String address;
        private boolean man;
    
    
        public static void printf(Person p) {
            // 查看对象的整体结构信息
            //JOL工具类
            System.out.println(ClassLayout.parseInstance(p).toPrintable());
        }
    
        public static void main(String[] args) {
            Person person = new Person();
            person.setName("小陈");
            person.setAddress("北京");
            person.setAge(18);
            Person children = new Person();
            children.setName("小小陈");
            children.setAge(1);
            person.setChildren(children);
    
            System.out.println(Integer.toHexString((int) VM.current().addressOf(person)));
            //输出对象内存地址
            System.out.println(GraphLayout.parseInstance(person).toPrintable());
            //打印对象布局信息
            printf(person);
    		//没啥用,在这打个断点而已
            System.out.println("zzz");
        }
    

    当指定分配策略为0时,输出结果如下:

    在这里插入图片描述

    父类引用类型->父类基本类型->子类引用类型->子类基本类型->填充字段。

    当指定分配策略为0时,输出结果如下:

    在这里插入图片描述

    父类基本类型->父类引用类型->子类基本类型->子类引用类型->填充字段。

    上述两个结果可以看到,当Person类继承了父类之后,无论使用哪种策略,永远都是父类的属性排在前面,然后才是子类属性,而且父类的引用类型的属性和子类的引用类型的属性是被分隔开的(中间穿插着基本类型)。

    因为父类引用和子类引用之间穿插的基本类型,那么就很可能导致原先一个内存行就可以放完引用类型而现在不得不分两个内存行存放的情况,这样gc在扫描时就不得不扫描多个内存行了,于是策略2便诞生了。

    当指定分配策略为2时,输出结果如下:

    在这里插入图片描述

    为了使父类的引用可以和子类的引用连续在一起,父类采用了策略0进行分配,而子类采用了策略1。这样就形成了,父类基本类型->父类引用类型->子类引用类型->子类基本类型的排序关系,一定程度上减少了GC夸内存行扫描的情况,提升了GC效率。

    再引申一个问题:

    如果父类属性定义为了满足8字节对齐而出现了间隙的话,子类小字段是否会穿插进去?

    举个栗子:

    //修改父类
    public class Biology {
        protected String type;
        protected int id;
        protected int rootId;
        protected boolean extince;
    }
    

    现在对象头12字节,父类id(int)4字节,父类rootId(int)4字节,然后extince(boolean)1字节,一共21字节为了满足8字节对齐,现在基本类型和引用类型之间就产生了3字节的间隙,子类现在有个man(boolean)1字节是否会插入进去,减少1字节的浪费呢?如果会的话,那么预期的结果就是下面这样的:

    OFFSET SIZE TYPE DESCRIPTION
    0 4 (object header)
    4 4 (object header)
    8 4 (object header)
    12 4 int Biology.id
    16 4 int Biology.rootId
    20 1 boolean Biology.extince
    21 1 boolean Person.man
    22 2 (loss due to the next object alignment)
    24 4 java.lang.String Biology.type
    28 4 com.cjf.test.Person Person.children
    32 4 java.lang.String Person.name
    36 4 java.lang.String Person.address
    40 8 long Person.height
    48 4 int Person.age
    52 4 (loss due to the next object alignment)
    Instance size: 56 bytes

    共计56字节,其中因为对齐填充产生了6字节的浪费。

    现在我们运行程序来验证一下;
    在这里插入图片描述

    虽然最后对象的大小的确为56,但是字段的排序并没有像我们预期的结果一样将子类1字节的字段插入到父类的间隙中。

    那么JVM为什么不把子类的小对象插入到父类的间隙中呢?难道是因为我们预期的分配策略最终产生了6字节的浪费,而不将子类属性插入间隙也是6字节的浪费,所以JVM觉得没有必要优化?那若果我们现在在子类中再定义一个4字节的属性,这样的话是不是以我们的分配方式就可以减少这4字节的浪费了?

    在子类中再加一个4字节大小的属性:

    @Data
    public class Person extends Biology{
        private Person children;
        private String name;
        private long height;
        private int age;
        private String address;
        private boolean man;
        private int areaCode;
    
        public static void printf(Person p) {
            // 查看对象的整体结构信息
            //JOL工具类
            System.out.println(ClassLayout.parseInstance(p).toPrintable());
        }
    }
    

    测试走起,结果如下:

    在这里插入图片描述

    很遗憾,JVM并没有把子类的小对象插入到父类间隙中;甚至为了向8字节对齐,不得不多浪费了4字节的地址空间,最终共造成了10字节的空间浪费。

    最后这个问题也留作思考吧;为什么JVM不把小对象插入到父类间隙之中呢?我查了很多资料,很多博主都说会,甚至深入理解JAVA虚拟机书中也写的会…

    至此,对象的探秘就结束了~Bye!

    展开全文
  • java对象对齐规则

    千次阅读 2019-09-22 15:52:30
    另外,特别重要的是,本文是基于hotspot来讨论的,不同的java虚拟机是有不同的,这一点,一定要注意。 一、什么是对象的内存布局 简单一句话:对象实例在jvm堆内存中存放的结构。就是随便实例化一个对象new ...

    零、注记

    本文是一次讨论的流水账,旨在讲明原理就行了,行文大家不要抱太大的希望。

    另外,特别重要的是,本文是基于hotspot来讨论的,不同的java虚拟机可能是有不同的,这一点,一定要注意。

     

    一、什么是对象的内存布局

    简单一句话:对象实例在jvm堆内存中存放的结构。就是随便实例化一个对象new Object(),他在堆内存里面是怎么放置的。

    看下面这个jol工具给出的java.math.BigInteger内存布局的例子:一个对象的内存布局包含了对象头object header、实例数据域和对齐填充alignment padding(可能有,可能没有,下面再细说)。

    ***** 64-bit VM, compressed references enabled: ***************************
    java.math.BigInteger object internals:
     OFFSET  SIZE  TYPE DESCRIPTION                    VALUE
          0    12       (object header)                N/A
         12     4   int BigInteger.signum              N/A
         16     4 int[] BigInteger.mag                 N/A
         20     4   int BigInteger.bitCount            N/A
         24     4   int BigInteger.bitLength           N/A
         28     4   int BigInteger.lowestSetBit        N/A
         32     4   int BigInteger.firstNonzeroIntNum  N/A
         36     4       (loss due to the next object alignment)
    Instance size: 40 bytes (estimated, the sample instance is not available)
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

     

    二、查看对象内存布局的工具

    1. openjdk jol

    openjdk官网给了一个查看对象内存布局的工具,jol(java object layout)http://openjdk.java.net/projects/code-tools/jol/

    怎么拿呢?openjdk给了maven的依赖:

    Use as Library Dependency
    OpenJDK Community semi-regularly pushes the releases to Maven Central. Therefore, you can use it right away by setting up the Maven dependency:
    <dependency>
        <groupId>org.openjdk.jol</groupId>
        <artifactId>jol-core</artifactId>
        <version>put-the-version-here</version>
    </dependency>
    It is a good idea to review JOL Samples and CLI tools source before using the tool at its full capacity as the library.

    怎么用呢?上面给的jol的链接页面,最下面官方给了jol samples的链接,使用极其简单,就是一个ClassLayout就没了。示例就懒得给了,看samples吧。

    jol sampleshttp://hg.openjdk.java.net/code-tools/jol/file/tip/jol-samples/src/main/java/org/openjdk/jol/samples/

    jol sourcecodehttp://central.maven.org/maven2/org/openjdk/jol/

    如果不想看samples呢?这篇参考文章给了用例和讲解《JDK之JVM中Java对象的头部占多少byte》https://my.oschina.net/u/2518341/blog/1838006

    那如果不想用jol工具怎么办呢?卧槽,我好难啊。。。

    2. sun.misc.Unsafe

    • sun.misc.Unsafe.objectFieldOffset方法获取第一个field的偏移地址(弊端:当对象头后面有padding的时候,你看不出来,什么时候有padding呢,下面会细说)
    • JDK8及之前,是用的sun.misc.Unsafe
    • JDK9有两个Unsafe,除了sun.misc.Unsafe还提供了jdk.internal.misc.Unsafe,但是jdk.internal.misc.Unsafe不像sun.misc.Unsafe是可以通过反射使用的,实际上目前在JDK9以后的版本中,sun.misc.Unsafe中组合了jdk.internal.misc.Unsafe的实例,实际上sun.misc.Unsafe是一个简单包装,你可以自己翻翻源码。

    至少有两种方式可以获取到sun.misc.Unsafe实例对象:

    • 通过反射sun.misc.Unsafe的构造函数获取其实例对象;
    • 通过反射sun.misc.Unsafe的实例属性theUnsafe获取其实例对象;
    package cn.wxy.unsafe;
    
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationTargetException;
    import sun.misc.Unsafe;
    
    public class UnsafeUtils {
    	/**
    	 * 通过反射sun.misc.Unsafe的构造函数获取其实例对象
    	 * 
    	 * @return sun.misc.Unsafe
    	 */
    	public static Unsafe getUnsafeByConstructor() {
    		Constructor<Unsafe> constructor = null;
    		try {
    			constructor = Unsafe.class.getDeclaredConstructor();
    			constructor.setAccessible(true);
    			return constructor.newInstance();
    		} catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException
    				| IllegalArgumentException | InvocationTargetException e) {
    			e.printStackTrace();
    		}
    		return null;
    	}
    
    	/**
    	 * 通过反射sun.misc.Unsafe的属性获取其实例对象
    	 * 
    	 * @return sun.misc.Unsafe
    	 */
    	public static Unsafe getUnsafeByField() {
    		Field field = null;
    		try {
    			field = Unsafe.class.getDeclaredField("theUnsafe");
    			field.setAccessible(true);
    			return (Unsafe) field.get(null);
    		} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
    			e.printStackTrace();
    		}
    		return null;
    	}
    }

     

    三、#program pack(n)

    C、C++里面的对齐规则,默认32bit机器是4byte对齐,64比特机器是8byte对齐。那如果想修改默认对齐规则呢?在源码开头写上#program pack(n)声明就行了。

    #program pack(n),n必须是2的次方,这个声明的作用就是告诉编译器使用的对齐方式是n(不管对齐方式n是1byte、2byte、4byte、8byte、16byte还是多少,对齐规则不变,都如下所示),就不再使用默认的对齐方式。

    在C、C++里面的对齐规则如下(链接:https://baike.baidu.com/item/%E5%86%85%E5%AD%98%E5%AF%B9%E9%BD%90/9537460?fr=aladdin):

    规则:

    1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行

    2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行

    3、结合1、2可推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。

    众所周知,jvm是C、C++写的,那java默认的8byte对齐规则,和这个一样吗?

    结果是有相同的部分,也有不同的部分:

    • 对象内的对齐规则:对象头、field和padding和第一条规则一致;
    • 对象间的对齐规则:不一样,java默认就是对象间8byte对齐,不管对象头的size、fields中最大的field的size是否小于8byte,如果是16byte,那对象之间就是按照16byte对齐,一样的也不管对象头的size、fields中最大的field的size是否小于16byte。

    第五部分,通过jol会给出两条规则的示例,更多的下面部分再细说。

    补充

    为什么要对齐?1. 效率;2. 有些OS平台有要求。

    参考链接:《Data alignment: Straighten up and fly right》https://developer.ibm.com/articles/pa-dalign/

     

    四、java对象的内存布局

    java对象的内存布局,在周志明的《深入理解java虚拟机》第二章有讲解,三个结构:

    • 对象头:mark word和元数据指针,注意如果是数组对象,其对象头除了mark word和元数据指针之外,还有个4byte int类型的length,本文未讨论数组类型(如果是数组类型,那么对象头加4byte,其他规则不变)。
    • 实例数据域
    • 对齐填充padding

    1. 对象头

    如果你理解hotspot的oop-klass二分模型,那这里你一定了解过。jvm中对象的对象头分为两部分,mark work和元数据指针。

    在hotspot的oop.hpp文件中class oopDesc描述了对象头,链接和源码如下:

    src/share/vm/oops/oop.hpp:http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/oops/oop.hpp

    class oopDesc {
      friend class VMStructs;
     private:
      volatile markOop  _mark;
      union _metadata {
        Klass*      _klass;
        narrowKlass _compressed_klass;
      } _metadata;

    其中,markOop _mark官方文档叫做mark word,union _metadata中的Klass* _klass是元数据指针,指向持久代或者metaspace中每个类的元数据,也就是java.lang.Class类实例访问的jvm中该类的数据结构。

    mark word的内存结构及源码如下,其中在32bit机器上是占4byte,在64bit机器上是8byte,不管是否开启压缩指针-XUseCompressOops。是否开启压缩指针,影响的是元数据指针_klass的size。

    src/share/vm/oops/markOop.hpp:http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/oops/markOop.hpp

    // Bit-format of an object header (most significant first, big endian layout below):
    //
    //  32 bits:
    //  --------
    //             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
    //             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
    //             size:32 ------------------------------------------>| (CMS free block)
    //             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
    //
    //  64 bits:
    //  --------
    //  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
    //  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
    //  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
    //  size:64 ----------------------------------------------------->| (CMS free block)
    //
    //  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
    //  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
    //  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
    //  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
    //
    //  - hash contains the identity hash value: largest value is
    //    31 bits, see os::random().  Also, 64-bit vm's require
    //    a hash value no bigger than 32 bits because they will not
    //    properly generate a mask larger than that: see library_call.cpp
    //    and c1_CodePatterns_sparc.cpp.
    //
    //  - the biased lock pattern is used to bias a lock toward a given
    //    thread. When this pattern is set in the low three bits, the lock
    //    is either biased toward a given thread or "anonymously" biased,
    //    indicating that it is possible for it to be biased. When the
    //    lock is biased toward a given thread, locking and unlocking can
    //    be performed by that thread without using atomic operations.
    //    When a lock's bias is revoked, it reverts back to the normal
    //    locking scheme described below.
    //
    //    Note that we are overloading the meaning of the "unlocked" state
    //    of the header. Because we steal a bit from the age we can
    //    guarantee that the bias pattern will never be seen for a truly
    //    unlocked object.
    //
    //    Note also that the biased state contains the age bits normally
    //    contained in the object header. Large increases in scavenge
    //    times were seen when these bits were absent and an arbitrary age
    //    assigned to all biased objects, because they tended to consume a
    //    significant fraction of the eden semispaces and were not
    //    promoted promptly, causing an increase in the amount of copying
    //    performed. The runtime system aligns all JavaThread* pointers to
    //    a very large value (currently 128 bytes (32bVM) or 256 bytes (64bVM))
    //    to make room for the age bits & the epoch bits (used in support of
    //    biased locking), and for the CMS "freeness" bit in the 64bVM (+COOPs).
    //
    //    [JavaThread* | epoch | age | 1 | 01]       lock is biased toward given thread
    //    [0           | epoch | age | 1 | 01]       lock is anonymously biased
    //
    //  - the two lock bits are used to describe three states: locked/unlocked and monitor.
    //
    //    [ptr             | 00]  locked             ptr points to real header on stack
    //    [header      | 0 | 01]  unlocked           regular object header
    //    [ptr             | 10]  monitor            inflated lock (header is wapped out)
    //    [ptr             | 11]  marked             used by markSweep to mark an object
    //                                               not valid at any other time
    //
    //    We assume that stack/thread pointers have the lowest two bits cleared.

    在64bit机器上,元数据指针的大小是会受压缩类指针是否开启的影响的。32bit机器,元数据指针大小4byte,在64byte机器上,默认是开启压缩指针的(-XX:+UseCompressedClassPointers),开启之后,元数据指针也是4byte,关闭则占8byte。

    补充:

    在Scott oaks写的《java性能权威指南》第八章8.22节提到了当heap size堆内存大于32GB是用不了压缩指针的,对象引用会额外占用20%左右的堆空间,也就意味着要40GB左右的内存才相当于开启了指针压缩的32GB堆空间。

    这是为什么呢?看下面引用中的红字(来自openjdk wiki:https://wiki.openjdk.java.net/display/HotSpot/CompressedOops)。开启压缩指针需要连续的地址空间(进程的虚拟地址空间),其实现原理了是64bit基址+32bit的偏移地址,32bit最大寻址空间是4GB,Java默认8byte对齐,所以开启了压缩指针之后呢,一个地址寻址不再是1byte,而是8byte,因为不管是32bit的机器还是64bit的机器,java对象都是8byte对齐的,而类是java中的基本单位,对应的堆内存中都是一个一个的对象。

    Compressed oops represent managed pointers (in many but not all places in the JVM) as 32-bit values which must be scaled by a factor of 8 and added to a 64-bit base address to find the object they refer to. This allows applications to address up to four billion objects (not bytes), or a heap size of up to about 32Gb. At the same time, data structure compactness is competitive with ILP32 mode.

    你还可以查看jvm中压缩指针参数文档,压缩指针有两个开关——-XX:+UseCompressedOops和-XX:+UseCompressedClassPoints,前者是压缩对象指针,后者是压缩类指针。

    -XX:-UseCompressedOops: 

    Disables the use of compressed pointers. By default, this option is enabled, and compressed pointers are used when Java heap sizes are less than 32 GB. When this option is enabled, object references are represented as 32-bit offsets instead of 64-bit pointers, which typically increases performance when running the application with Java heap sizes less than 32 GB. This option works only for 64-bit JVMs.

    It is also possible to use compressed pointers when Java heap sizes are greater than 32GB. See the -XX:ObjectAlignmentInBytes option.

    为什么坚持8byte对齐呢?Scott oaks在书上给了理由:

    2. 实例数据域

    实例数据域紧跟在对象头之后。一个类没有field,就不需要实例数据域,有那就按照第三部分#program pack对齐规则的第一条放在堆内存中。这部分在第五部分,给出jol的示例详细讨论其规则。

    3. padding

    对象内可以有padding也可以没有;对象间默认按照8byte对齐,对齐则不需要padding,否则需要padding补充8byte对齐,也在第五部分根据jol的示例详细讨论其规则。

     

    五、java的对齐规则

    1. 对象内的对齐规则:对象头、field和padding和#program pack第一条规则一致;
      1. 注意:对象头默认是没有padding的,是否需要padding,要看是32bit机器还是64bit机器,以及对象头后面跟的field size,这是#program pack的对齐规则导致的padding,而不是对象头导致padding,这一点看过很多人讨论错了。
    2. 对象间的对齐规则:不一样,java的对象间对齐如果是按照8byte,那就是8byte,不会再像#program pack中还需要和对象内最大size的属性比较;
      1. 解释:java默认就是对象间8byte对齐,不管对象头的size、fields中最大的field的size是否小于8byte;如果是16byte,那对象之间就是按照16byte对齐,一样的也不管对象头的size、fields中最大的field的size是否小于16byte。
      2. -XX:ObjectAlignmentInBytes=alignment:你可以通过这个JVM参数修改对象的对齐方式

     

    -XX:ObjectAlignmentInBytes=alignment

    Sets the memory alignment of Java objects (in bytes). By default, the value is set to 8 bytes. The specified value should be a power of two, and must be within the range of 8 and 256 (inclusive). This option makes it possible to use compressed pointers with large Java heap sizes.

    The heap size limit in bytes is calculated as:

    4GB * ObjectAlignmentInBytes

    Note: As the alignment value increases, the unused space between objects will also increase. As a result, you may not realize any benefits from using compressed pointers with large Java heap sizes.

    补充:前四个例子均来源于jol官网示例,我感觉足够说明了,就懒得动手了,第五个需要动手搞一下,但是我也没动手 ,直接用了和群友讨论的例子。感觉足够说明了,就偷懒取巧不动手了。

    例一、32bit机器的对象内存布局(默认对象间8byte对齐)

    ***** 32-bit VM: **********************************************************
    $ java -jar jol-cli/target/jol-cli.jar estimates java.math.BigInteger
    java.math.BigInteger object internals:
     OFFSET  SIZE  TYPE DESCRIPTION                    VALUE
          0     8       (object header)                N/A
          8     4   int BigInteger.signum              N/A
         12     4 int[] BigInteger.mag                 N/A
         16     4   int BigInteger.bitCount            N/A
         20     4   int BigInteger.bitLength           N/A
         24     4   int BigInteger.lowestSetBit        N/A
         28     4   int BigInteger.firstNonzeroIntNum  N/A
    Instance size: 32 bytes (estimated, the sample instance is not available)
    Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

    32bit机器上,对象头8byte,其中,mark word 4byte,元数据指针4byte。

    java.math.BigInteger有6个成员属性field,都是4byte(5个int基本数据类型每个4byte;一个int[]数组引用类型,这里其实保存的是指针,因为数组本身就是一个引用类型,所以在堆内存中还有自己对象结构,因此在本例BigInteger中的int[]其实是一个4byte指针),按照4byte对齐,对象头0~7byte,后面的每个field的起始地址都是4byte的整倍数,不需要额外的padding来对齐。

    对象大小:object header(mark word+metadata klass)+6*field = 8byte+4*6byte =32byte。

    因为32byte % 8byte= 0,所以对象间不需要额外的padding来帮助对齐。

     

    例二、64bit机器的对象内存布局(默认对象间8byte对齐,没有开启指针压缩)

    ***** 64-bit VM: **********************************************************
    java.math.BigInteger object internals:
     OFFSET  SIZE  TYPE DESCRIPTION                    VALUE
          0    16       (object header)                N/A
         16     8 int[] BigInteger.mag                 N/A
         24     4   int BigInteger.signum              N/A
         28     4   int BigInteger.bitCount            N/A
         32     4   int BigInteger.bitLength           N/A
         36     4   int BigInteger.lowestSetBit        N/A
         40     4   int BigInteger.firstNonzeroIntNum  N/A
         44     4       (loss due to the next object alignment)
    Instance size: 48 bytes (estimated, the sample instance is not available)
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

    64bit机器,未开启指针压缩,对象头16byte,其中mark word 8byte,元数据指针8byte。

    java.math.BigInteger有6个成员属性,其中int[] BigInteger.mag是引用类型,未开启指针压缩占8byte,剩下5个int各占4byte,对象头0~15byte,mag 8byte对齐,占16~23,剩下的5个4byte对齐,对象内不需要额外的padding对齐。

    对象大小:16byte+8byte+5*4byte = 44byte。

    因为44byte%8byte = 4byte,所以按照8byte对齐,对象间还需要额外的4byte来帮助对齐。

    所以,对象真正占用的内存是44byte + 4byte = 48byte。

     

    例三、64bit机器并开启压缩指针的对象内存布局(默认对象间8byte对齐)

    ***** 64-bit VM, compressed references enabled: ***************************
    java.math.BigInteger object internals:
     OFFSET  SIZE  TYPE DESCRIPTION                    VALUE
          0    12       (object header)                N/A
         12     4   int BigInteger.signum              N/A
         16     4 int[] BigInteger.mag                 N/A
         20     4   int BigInteger.bitCount            N/A
         24     4   int BigInteger.bitLength           N/A
         28     4   int BigInteger.lowestSetBit        N/A
         32     4   int BigInteger.firstNonzeroIntNum  N/A
         36     4       (loss due to the next object alignment)
    Instance size: 40 bytes (estimated, the sample instance is not available)
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

    64bit机器开启指针压缩,对象头12byte,其中mark word 8byte,元数据指针4byte。

    java.math.BigInteger 6个成员属性,因为开启了指针压缩,所以例二中的int[] BigInteger.mag不再占用8byte,而是4byte,其他5个成员属性int各占4byte。对象头12byte,每个成员属性按照4byte对齐,对象内不需要额外的padding来帮助对齐。

    对象大小:12byte + 6*4byte = 36byte。

    因为36byte % 8byte = 4byte,按照8byte对齐,所以对象间还需要额外的4byte来帮助对齐。

    所以对象真正占用的内存大小:36byte + 4byte = 40byte。

     

    例四、64bit机器并开启压缩指针、修改按照16byte对齐

    ***** 64-bit VM, compressed references enabled, 16-byte align: ************
    java.math.BigInteger object internals:
     OFFSET  SIZE  TYPE DESCRIPTION                    VALUE
          0    12       (object header)                N/A
         12     4   int BigInteger.signum              N/A
         16     4 int[] BigInteger.mag                 N/A
         20     4   int BigInteger.bitCount            N/A
         24     4   int BigInteger.bitLength           N/A
         28     4   int BigInteger.lowestSetBit        N/A
         32     4   int BigInteger.firstNonzeroIntNum  N/A
         36    12       (loss due to the next object alignment)
    Instance size: 48 bytes (estimated, the sample instance is not available)
    Space losses: 0 bytes internal + 12 bytes external = 12 bytes total

    对象按16byte对齐,也就意味着对象放到堆中的时候,其起始地址模16必须为0,即address % 16byte = 0。

    对象头和例三一样,12byte。

    开启指针压缩,对象内6个属性也都是4byte,对象内各个属性按照4byte对齐,不需要额外的padding。

    对象大小:12byte + 6*4byte = 36byte。

    因为36byte % 16byte = 4byte,为了模16byte为0,对象间还需要额外的12byte来帮助对齐(12byte + 4byte = 16byte)。

    所以对象真正占用的的内存大小:36byte+12byte(对象间的alignment)=48byte。

     

    例五、对象头是否需要padding

    这个对比例子中,java.lang.Integer对比java.lang.Long。Integer中只有一个private final int value的对象,Long中只有一个private final long value属性(就是JDK的源码)。

    环境:64bit机器开启压缩指针,默认按照8byte对齐。

    Integer的例子中,对象头12byte,属性int value 4byte并按照4byte对齐,所以最终对象大小16byte。对象内的属性int value起始地址12,所以对象内不需要额外的padding,对象大小16byte,是8byte的整倍数,所以对象间也不需要额外的padding来对齐。

    Long的例子中,对象头12byte,long value是8byte并按照8byte对齐,而对象头12byte从0byte~11byte,所以Long的实际存放地址是16~23byte,在long value和对象头之间需要4byte的padding(但这个padding不是对象头的,是后面的long value根据第一条对齐规则导致的,Integer例子中int value是4byte对齐,就不需要额外的padding)。填充之后,对象大小为24byte,是默认对齐8byte的整倍数,对象间不需要额外的padding。

     

    六、附注

    以上就是根据表象推断出来的java的对齐规则。可能有遗漏,甚至有错误,欢迎指正,欢迎留言讨论!

    展开全文
  • 对齐填充的目的

    2021-05-30 11:16:25
    没有对齐填充时,A线程只想要读取一个long类型的变量a,但a只占8字节,因此会连带变量b、c、d也会一并读取到缓存行中;而同一时刻B线程只想要读取一个long类型的变量b,但b只占8字节,因此会连带变量a、c、d也会一并...
  • ①. 堆的概述 1>. 堆的概述(共享|垃圾回收) ①. 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域 ...②. Java堆区在JVM启动的时候即被创建,... 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的
  • 文章目录①. 对象在堆内存中的存储布局②.... 对象内部结构分为:对象头、实例数据、对齐填充(保证8个字节的倍数)。 ②. 对象头分为对象标记(markOop)和类元信息(klassOop),类元信息存储的是指向该对象类元数据(kla
  • 1.整体上来看,对象在内存中有3部分,对象头、实例数据、对齐填充。 a)对象头包含运行时元数据和类型指针。运行时元数据包含哈希值(这个哈希值就是创建出来的对象在内存中的地址)、GC分代年龄、锁状态标志等。类型...
  • JAVA字节对齐详解

    2021-05-14 22:37:08
    在HotSpot虚拟机中,对象在内存中的存储布局分为三块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding) 对象头 Mark Word:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位等等。...
  • JVM成神之路-Java对象模型

    千次阅读 2018-07-23 15:01:17
    一个Java对象可以分为三部分存储在内存中,分别是:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。 对象头(包含锁状态标志,线程持有的锁等标志) 实例数据 对齐填充 oop-klass model(...
  • java对象头信息

    千次阅读 多人点赞 2019-09-02 14:27:14
    1. 一个java对象到底占用了多少内存空间,应该如何计算? 2. 为什么在jdk1.6后,synchronized关键字性能有所提高,为什么会提高?并且很多文章中都说synchronized锁有偏向锁、轻量锁、重量锁等状态? 3. java对象...
  • Java对象在内存中的布局

    千次阅读 2019-08-08 00:43:17
    在虚拟机中,对象在内存中的存储布局可分为三块:对象头、实例数据和对齐填充 1、对象对象头用于存储对象的元数据信息 对象头又可以分为两块内容:第一部分用于存储对象自身的运行时数据,如哈希码...
  • 对象是在堆中创建的 对象的内部结构图为
  • java对象结构

    万次阅读 多人点赞 2017-04-19 22:31:57
    在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。下图是普通对象实例与数组对象实例的数据结构: 对象头 HotSpot虚拟机的...
  • 一、对象的创建过程 二、对象的内部结构(内存分配) ...3、对齐填充   起着占位符的作用,实例数据起始地址必须是8字节的整数倍,(对像的大小必须是8字节的整数倍)。 三、对象的访问   通过栈上的refe...
  • Java对象内存大小计算

    万次阅读 2018-07-03 14:43:34
    最近在读《深入理解Java虚拟机》,对Java对象的内存布局有了进一步的认识,于是脑子里自然而然就有一个很普通的问题,就是一个Java对象到底占用多大内存? 在网上搜到了一篇博客讲的非常好:...
  • java 对象结构笔记

    2019-07-26 18:19:21
    java对象结构主要分为对象头,数据和填充字节 二、对象头 对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳...
  • Java对象结构详解

    2019-11-18 19:34:52
    synchronized (obj) { ...要弄清楚这个问题,就有必要了解一下在JVM虚拟机中一个Java对象是怎么存在的,换句话说就是在虚拟机中用什么结构来表示一个Java对象,或者一个Java对象的组成结构是什么样的。 划重点~~...
  • Java对象创建的流程

    千次阅读 2019-05-07 22:05:39
    文章目录Java对象创建的流程1.Java普通对象的创建1.1new指令1.2分配内存1.3初始化1.4对象的初始设置1.5\方法2.Java对象内存布局2.1对象头2.2实例数据2.3对齐填充 1.Java普通对象的创建 这里讨论的仅仅是普通Java...
  • 关于java中输出格式对齐的问题

    千次阅读 2021-05-18 21:45:09
    输出格式的对齐 1.\t的使用 一般在输出时可能会出现需要格式对齐情况,例如以下的日历输出(已经调整过) 但再这之前使用了制表符\t来对齐, \t用法 1 .\t 表示制表符,相当于制表符 ...import java.util.Cal
  • Java对象内存模型 前言 我们的一个Java对象在内存中究竟长什么样子,我们类文件最终会被编译为字节码文件,然后被类加载器加载,并加入到内存。我们的字节码文件是个二进制文件,虽然我们可以通过可以把.class文件...
  • java对象的内存组成 java对象的内存以字节为单位,且必须是8的倍数,它的构成由3部分组成: 对象头: mark word(8字节/64bit)、oop指针...对齐内存填充: 整个对象的字节数必须是8的倍数,不足则需要补充 其...
  • Java 程序中,我们拥有多种新建对象的方式。除了最为常见的 new 语句之外,我们还可以通过反射机制、Object.clone 方法、反序列化以及 Unsafe.allocateInstance 方法来新建对象。 其中,Object.clone 方法和反...
  • 今天闲来无事,有空闲的时间,所以想坐下来聊一聊Java的GC以及Java对象在内存中的分配。 和标题一样,本篇绝对是用最直接最通俗易懂的大白话来聊 文章中基本不会有听起来很高大上专业术语,也不会有太多概念性的...
  • 文章目录 1 java对象布局(hotspot虚拟机)简介 1.2 实例数据 --- 可以没有 1.3 对齐填充 --- 有可能会没有 1.4 对象头 1.4.1 对象头在JDK(hotspot)源码中具体的体现 1.4.2 Mark Word 1.4.2 klass pointer 1.4.3 ...
  • 3.对齐填充(可能没有,因为 java 中规定对象的起始地址必须是 8 bytes 的正数倍) 对于普通对象而言,对象头中包括 mark word(8 bytes)、kclass(没有开启压缩的时候是 8 bytes,开启压缩了的话,就是 4 bytes),如果...
  • Java对象的内存布局与压缩指针原理

    千次阅读 2020-06-20 19:24:05
    一个Java对象,依据Hotspot的实现来讲,分为三块区域:对象头,实例数据,对齐填充块,如下图 首先来认识下对象头 对象头(Header) 对象头由两部分组成:一部分是Markword,另一部分是类型指针; Markword在32位...
  • Java对象头和对象组成详解

    万次阅读 多人点赞 2018-07-20 10:46:47
    Java对象保存在内存中时,由以下三部分组成: 1,对象头 2,实例数据 3,对齐填充字节 一,对象头 java的对象头由以下三部分组成: 1,Mark Word 2,指向类的指针 3,数组长度(只有数组对象才有)   ...
  • 目录 一、JAVA内存结构 1.1 JVM启动流程: 1.2 JVM基本结构 1.2.1基本结构图 1.2.2 Java中的内存分配 ...2.3 java内存模型对并发提供的保障:原子性、可见性。...三、Java对象模型 3.1 oop-klass...
  • 3)、对齐填充字节 二、对象头 java对象头有以下三部分组成: 1)、Mark Word 2)、Class Metadata Address(指向类的指针) 3)、Array Length(数组长度,只有数组对象才有) JVM中对象头的方式有以下两种...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 18,520
精华内容 7,408
关键字:

java对象对齐填充

java 订阅