精华内容
下载资源
问答
  • ​ 语言层面上,创建对象通常(例外:复制、反序列化)仅仅是一个new关键字而已,本文所探讨的虚拟机对象创建不包含数组和Class对象等,就对于普通对象而言。 常量池中定位符号引用 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虚拟机通过这个指针来确定该对象是哪个类的实例。

    展开全文
  • 上一篇文章我们讲解了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!

    展开全文
  • 对象在堆内存中的存储布局可以划分三个部分:对象头(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倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

    展开全文
  • ①. 堆的概述 1>. 堆的概述(共享|垃圾回收) ①. 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域 ②. Java堆区在JVM启动的时候...③....④....⑤. 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的

    前言:
    (1).new 最常见的方式 | 变形1 : Xxx的静态方法 | 变形2 : XxBuilder/XxoxFactory的静态方法
    (2).Class的newInstance():反射的方式,只能调用空参的构造器,权限必须是public
    (3).Constructor的newInstance(Xxx):反射的方式,可以调用空参、带参的构造器,权限没有要求
    (4).使用clone() :不调用任何构造器,当前类需要实现Cloneable接口,实现clone()
    (5).使用反序列化:从文件中、从网络中获取一个对象的二进制流
    (6).第三方库Objenesis

    在这里插入图片描述

    ①. 从字节码角度看待对象的创建过程

    • ①. 从最简单的Object ref = new Object()在这里插入图片描述

    • ②. new:如果找不到Class对象,则进行类加载。加载成功后,则在堆中分配内存,从Object 开始到本类路径上的所有属性值都要分配内存。分配完毕之后,进行零值初始化。在分配过程中,注意引用是占据存储空间的,它是一个变量,占用4个字节。这个指令完毕后,将指向实例对象的引用变量压入虚拟机栈顶。

    • ③. dup:在栈顶复制该引用变量,这时的栈顶有两个指向堆内实例对象的引用变量。如果 方法有参数,还需要把参数压人操作栈中。两个引用变量的目的不同,其中压至底下的引用用于赋值,或者保存到局部变量表,另一个栈顶的引用变量作为句柄调用相关方法。

    • ④. invokespecial:调用对象实例方法,通过栈顶的引用变量调用init方法。
      补充:clinit是类初始化时执行的方法, 而init 是对象初始化时执行的方法。

    ②. 对象的实例化(六个步骤)

    • ①. 判断对象对应的类是否加载、链接、初始化
      (虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。( 即判断类元信息是否存在)。如果没有,那么在双亲委派模式下,使用当前类加载器以ClassLoader+包名+类名为Key进行查找对应的.class文件。如果没有找到文件,则抛出ClassNotFoundException异常,如果找到,则进行类加载,并生成对应的Class类对象)

    • ②. 为对象分配内存:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象。 如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小
      (byte、int、float、引用数据类型4个字节大小 | double、long 占八个字节)

    1. 如果内存规整,使用指针碰撞
      如果内存是规整的,那么虚拟机将采用的是指针碰撞法(BumpThePointer)来为对象分配内存。意思是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带有compact (整理)过程的收集器时,使用指针碰撞。
    2. 如果内存不规整,虚拟机需要维护一个列表,使用空闲列表分配(CMS)
      如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虛拟机将采用的是空闲列表法来为对象分配内存。意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为“空闲列表(Free List)
    3. 说明:选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
      在这里插入图片描述在这里插入图片描述
    • ③. 处理并发安全问题
      (在分配内存空间时,另外一个问题是及时保证new对象时候的线程安全性:创建对象是非常频繁的操作,虚拟机需要解决并发问题。虚拟机采用 了两种方式解决并发问题:)
    1. CAS ( Compare And Swap )失败重试、区域加锁:保证指针更新操作的原子性
    2. TLAB把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区,(TLAB ,Thread Local Allocation Buffer) 虚拟机是否使用TLAB,可以通过一XX:+/一UseTLAB参数来 设定
    • ④. 初始化分配到的空间:赋予默认的初始化值;比如int=0| boolean=false(默认的值)

    • ⑤. 设置对象的对象头:将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。

    • ⑥. 执行init方法进行初始化(进行赋值的处理)
      (在Java程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。因此一般来说(由字节码中是否跟随有invokespecial指令所决定),new指令之 后会接着就是执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。)

    • ⑦. 代码展示

    /**
     * 测试对象实例化的过程
     *  ① 加载类元信息 - ② 为对象分配内存 - ③ 处理并发问题  - ④ 属性的默认初始化(零值初始化)
     *  - ⑤ 设置对象头的信息 - ⑥ 属性的显式初始化、代码块中初始化、构造器中初始化
     *
     *  给对象的属性赋值的操作:
     *  ① 属性的默认初始化 - ② 显式初始化 / ③ 代码块中初始化 - ④ 构造器中初始化
     * 
     */
    public class Customer{
        int id = 1001;
        String name;
        Account acct;
    
        {
            name = "匿名客户";
        }
        public Customer(){
            acct = new Account();
        }
    
    }
    
    class Account{
    
    }
    

    ③. 对象的内存布局

    • ①. 对象内部结构分为:对象头、实例数据、对齐填充(保证8个字节的倍数)

    • ②. 对象头分为对象标记(markOop)和类元信息(klassOop),类元信息存储的是指向该对象类元数据(klass)的首地址
      在这里插入图片描述

    ①. 对象头(Header)

    • ①. 对象标记Mark Word 默认存储 (哈希值(HashCode )、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳)等信息
    1. 这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。
    2. 它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化。
      在这里插入图片描述
    • ②. 对象头多大 在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节
      在这里插入图片描述
    • ③. 类元信息(又叫类型指针) 对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例

    ②. 实例数据(Instance Data)

    • 说明:它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段) 规则:
    1. 相同宽度的字段总被分配在一起
    2. 父类中定义的变量会出现在子类之前
    3. 如果CompactFields参数为true(默认为true),子类的窄变量可能插入到父类变量的空隙

    ③. 对齐填充(Padding)

    • ①. 不是必须的,也没特别含义,仅仅起到占位符作用

    • ②. 解释如下图:

    在这里插入图片描述

    ④. 总结

    • ①. 代码演示
    public class CustomerTest {
        public static void main(String[] args) {
            Customer cust = new Customer();
        }
    }
    
    • ②. 图解代码
      在这里插入图片描述

    ④. 对象的访问定位

    前言:
    JVM是如何通过栈帧中的对象引|用访问到其内部的对象实例的呢?-> 定位,通过栈上reference访问

    • ①. 句柄访问
      在这里插入图片描述
    • ②. 直接指针(HotSpot采用)
      在这里插入图片描述

    ⑤. 直接内存(Direct Memory)

    • ①. 不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域

    • ②. 直接内存是Java堆外的、直接向系统申请的内存区间

    • ③. 代码演示:

    /**
     *  IO                  NIO (New IO / Non-Blocking IO)
     *  byte[] / char[]     Buffer
     *  Stream              Channel
     *
     * 查看直接内存的占用与释放
     */
    public class BufferTest {
        private static final int BUFFER = 1024 * 1024 * 1024;//1GB
    
        public static void main(String[] args){
            //直接分配本地内存空间
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
            System.out.println("直接内存分配完毕,请求指示!");
    
            Scanner scanner = new Scanner(System.in);
            scanner.next();
    
            System.out.println("直接内存开始释放!");
            byteBuffer = null;
            System.gc();
            scanner.next();
        }
    }
    
    • ④. 来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存

    在这里插入图片描述
    在这里插入图片描述

    • ⑤. 通常,访问直接内存的速度会优于Java堆。即读写性能高

    • ⑥. 直接内存大小可以通过MaxDirectMemorySize设置,如果不指定,默认与堆的最大值一Xmx参数值一致

    • ⑦. 简单理解: java process memory = java heap + native memory
      在这里插入图片描述

    展开全文
  • 1.整体上来看,对象在内存中有3部分,对象头、实例数据、对齐填充。 a)对象头包含运行时元数据和类型指针。运行时元数据包含哈希值(这个哈希值就是创建出来的对象在内存中的地址)、GC分代年龄、锁状态标志等。类型...
  • java对象对齐规则

    千次阅读 2019-09-22 15:52:30
    零、注记 ...一、什么对象的内存布局 简单一句话:对象实例在jvm堆内存中存放的结构。就是随便实例化一个对象new Object(),他在堆内存里面是怎么放置的。 看下面这个jol工具给出的java.mat...
  • “数据项只能存储在地址是数据项大小的整数倍的内存位置上(分别偶地址、被4整除的地址、被8整除的地址 )” 例如int类型占用4个字节,地址只能在0,4,8等位置上。 例1: #include struct xx{  char b;
  • 小甲鱼零基础入门学习python笔记

    万次阅读 多人点赞 2019-08-14 11:06:30
    应用范围:操作系统、WEB、3D动画、企业应用、云计算 大家可以学到什么:Python3的所有常用语法、面向对象编程思维、运用模块进行编程、游戏编程、计算机仿真 Python 是脚本语言 脚本语言(Scripting language)是...
  • # +是填充字符 >是右对齐 30是宽度 .3f是保留小数点后3位 #若平凡根后是一个复数,复数的实部和虚部都是浮点数,.3f可以将实部和虚部分别取三位小数 字符串分段组合 :获得输入的一个字符串s,以字符减号(-)...
  • 若认可本篇博客,希望给一个点赞、收藏 并且,遇到了什么问题,请在评论区留言,我会及时回复的 这本书对Python的知识点的描述很详细,而且排版看的很舒服 几个例题: 假装自己从零开始学,将一些有代表性、有意思的...
  • str对象包括如下用于填充,空白,对齐字符串的方法 str.strip([char]):去两边空格,也可指定去除的字符列表 str.lstrip([char]):去左边空格,也可指定去除的字符列表 str.rstrip([char]):去右边空格,也可指定...
  • JVM_虚拟机目录

    万次阅读 多人点赞 2019-06-28 11:44:22
    JVM06_方法区的概述、内部结构、演变、常量池、运行时常量池、垃圾回收 JVM07_ 对象的实例化、内存布局(对象头、实例数据、对齐填充)、访问定位、直接内存 JVM08_字符串常量池基本特性、内存分配、拼接操作、intern...
  • 前端面试题

    万次阅读 多人点赞 2019-08-08 11:49:01
    这些浏览器的内核分别是什么? 21 每个HTML文件里开头都有个很重要的东西,Doctype,知道这是干什么的吗? 21 Quirks模式是什么?它和Standards模式有什么区别 21 div+css的布局较table布局有什么优点? 22 img的alt...
  • c++对象长度之内存对齐(2)

    千次阅读 2020-12-14 15:41:13
    内存对齐 ...编译器的指定对齐值,因为结构体中的当前数据成员类型长度M,指定的对齐值是N,那么实际的对齐q=min(M,N),其成员的地址安排在q的倍数上。 但是,并非设定了默认对齐值就将结构体的对
  • WPF学习

    万次阅读 多人点赞 2019-03-05 22:00:17
    1.4 使用Attribute为对象属性赋值 Rectangle类对象Fill属性的类型是Brush,Brush是个抽象类的单色画刷,实际上编译成: SoildColorBrush sBrush=new SoildColorBrush(); sBrush.Color=Color.Blue;...
  • 对齐对象的关键点。 安装 用安装 $ npm i right-align-keys --save 用法示例 align ( { a : 'x' , bbb : 'x' , ccccc : 'x' , ddddddd : 'x' } ) ; 结果是: { ' a' : 'x' , ' bbb' : 'x' , ' ...
  • C#基础教程-c#实例教程,适合初学者

    万次阅读 多人点赞 2016-08-22 11:13:24
    本章介绍C#语言的基础知识,希望具有C语言的读者能够基本掌握C#语言,并以此基础,能够进一步学习用C#语言编写window应用程序和Web应用程序。当然仅靠一章的内容就完全掌握C#语言是不可能的,如需进一步学习C#语言...
  • 用turtle实现用正方形画圆

    千次阅读 多人点赞 2018-11-19 13:34:54
    align – one of the strings “left”, “center” or right” 对齐参数3选1(left,right,center) font – a triple (fontname, fontsize, fonttype) 字体 乌龟状态 可视性 showturtle() | st() 显示...
  • python--format

    千次阅读 多人点赞 2019-01-16 15:11:31
    格式化之format1、位置参数2、关键字参数3、通过对象的属性4、使用对象下标5、填充对齐 #字符串通过format进行格式化 1、位置参数 print('{} , {}'.format(1,2)) print('{0} , {1}'.format(1,2)) print('{0} , ...
  • C++ 字节对齐的总结(原因和作用)

    万次阅读 多人点赞 2017-11-09 11:21:42
    什么是字节对齐  现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据...
  • WPF开发教程

    万次阅读 多人点赞 2019-07-02 23:13:20
    任何对象都可以任何其他对象提供属性定义,而不是将所有这些属性与每个元素相关联。这与 JavaScript 中的“expando”功能相似。 System.Windows.Media.Visual 定义一个系统后,下一步是将像素绘制到屏幕上...
  • Python xlwt 模块 Excel表格 基础 3:单元格格式、字体格式、对齐方式、边框及填充等前言:1、数据类型设置2、字体设置3、单元格对齐方式4、单元格边框设置5、填充设置6、单元格保护最后: 前言: 前面两节博客介绍...
  • 对象在内存中的布局,主要有3个组成部分,包括对象头,实例数据与对齐填充。确定对象的大小,也是从这3个组成部分的入手。 对象头 其中对象头中又包括Mark Word与Klass Word。当该对象是一个数组时,对象头还会...
  • 计算机图形学导论

    千次阅读 多人点赞 2018-10-12 22:42:46
    1.1什么是计算机图形学?(Computer Graphics) 关于计算机图形学的定义众说纷纭。 IEEE 对计算机图形学的定义:Computer graphicsis the art or science of producing graphical images with the aid of ...
  • C语言结构体的字节对齐原则
  • pandas的算术运算和数据对齐

    千次阅读 2018-01-06 19:34:55
    pandas可以对不同索引的对象进行算术运算,如果存在不同的索引对,结果的索引就是该索引对的并集。 一、算术运算 a、series的加法运算 s1 = Series([1,2,3],index=["a","b","c"]) s2 = Series([4,5,6],index=...
  • 内存对齐详解

    千次阅读 2020-04-25 11:56:22
    为什么要进行内存对其? (1)空间原因:没有进行内存对齐的结构体或类会浪费空间。 (2)性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而...
  • 彻底搞清计算结构体大小和数据对齐原则  By Qianghaohao    数据对齐:  许多计算机系统对基本数据类型合法地址做出了一些限制,要求某种类型对象的地址必须是  某个值K

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 61,534
精华内容 24,613
关键字:

对象为什么要对齐填充