堆空间-对象创建

对象创建

对象的组成部分

  • 对象头(Header)

  • 实例数据(Instance Data)

  • 对齐填充(Padding)

对象头

对象头图示

对象组成

对象头详解

  • [Mark Word]{.red}:

    • 定义:采用 32位/64位 的 Bitmap 数据结构存储对象的运行时相关信息

      :::info

      32位虚拟机的 Bitmap 数据结构就是32位的,64位的虚拟机的 Bitmap 就是64位的

      :::

    • 组成:

      • 问题:对象的运行时相关信息数据量特别多,超过了 Bitmap 数据结构所能承载的容量,那要怎么解决这个问题呢?
      • 方式:[Mark Word 被设计成为动态的数据结构:根据对象不同的锁状态确定 Mark Word 存储的具体内容]{.red}
    • 细节:

      • [未锁定状态和偏向锁状态的标志位是一样的,所以这两个锁还需要使用 1bit 来进行区分]{.blue}
      • 各个不同的锁不在此细讲,因为涉及到并发编程
    Mark-Word
  • [类型指针]{.red}

    • 定义:[指向该对象所属的类型元数据(Klass 对象)的指针]{.red}

    • 细节:

      • [对象可以通过类型元数据指针找到其所属的类,但是并不是所有对象都必须保存该指针]{.red}

        比如在使用反射中,你可以使用对象获取 Class 对象也可以使用类或者方法获取 Class 对象

      • [首先明确 Klass 对象不是 Class 对象]{.red}

        详细了解:[OOP-Klass模型](#OOP-Klass 模型)

  • 数组长度:用于记录数组长度的部分([只有数组才有这部分内容]{.red})

实例数据

  • 定义:存储对象中所有的有效信息

  • 组成:我们在程序中 [定义各种类型的 实例变量 以及从父类继承的 实例变量 ]{.red}

    +++warning 对象的实例数据仅包含各种各样的实例变量,并不包含方法信息

    ① 每个类都可以拥有非常多的对象,每个对象仅有数据字段不同,使用的方法肯定是完全相同的

    ② 所以让每个堆中的对象都记录虚方法表显然是不合适的,浪费堆内存空间

    ③ 既然对象中并不保存方法信息,那么到底哪里保存方法相关的信息呢?这个就涉及到 OOP-Klass 模型了

    +++

  • 存储顺序:[长度相同的变量总是优先分配在一起]{.red}

    • 虚拟机默认分配策略:
      • double & long 一起分配(8B)
      • int & float 一起分配(4B)
      • char & short 一起分配(2B)
      • byte & boolean 一起分配(1B)
      • referrence 最后分配(4B)
    • 虚拟机的分配策略还会受到 [变量的定义顺序]{.blue} 和 [分配策略参数]{.blue} 的影响

对其填充

  • 前提:
    • Java 虚拟机要求对象的大小必须是 8字节 的整数倍
    • 对象大小不是 8字节 整数倍时就会采用对其填充的方式变为 8 字节整数倍
  • 定义:为对象填充某些数据使得其大小为 8字节 的整数倍
  • 细节:[对其填充本身没有什么含义,但是会引发一系列的问题]{.blue}

对象头组成代码演示

:::info

① 使用 jol 工具类就可以查看对象在内存中的布局

② 使用 lombok 插件可以简化对象类的编写

:::

对象代码

package chapter08;

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Person
{
Integer age;
String name;
Boolean gender;
String work;

}

测试代码

public static void main(String[] args)
{
Person person = new Person(16, "冬坂五百里", true, "开机兵");
// 貌似只有使用到了哈希码才会生成, 否则在 MarkWord 中是看不到的
printfObject(person);
}

// 工具方法: 输出对象的内存布局
private static void printfObject(Person person)
{
// 输出对象头
System.out.println(ClassLayout.parseInstance(person).toPrintable());
System.out.println(GraphLayout.parseInstance(person).toPrintable());
}

测试结果

对象组成演示

OOP-Klass 模型

:::primary

参考博客:

【理解HotSpot虚拟机】对象在jvm中的表示:OOP-Klass模型

oop-klass内存模型

:::

什么是 OOP-Klass 模型

  • 定义:OOP 模型 + Klass 模型

  • OOP 模型(Ordinary Object Pointer):

    • 定义:[存储 Java 对象所有实例数据的模型]{.red}
    • 创建时间:创建对象时(对象实例化)就会相应的创建 OOP 对象
    • 组成:对象头 + 实例数据 + 数组长度
    • 存储位置:[堆空间]{.red}

    :::info

    说白了,OOP 就是我们创建对象后,堆空间存储对象相关数据的模型

    :::

  • Klass 模型:

    • 定义:[存储 Java 类的所有元数据信息]{.red}
    • 创建时间:类加载阶段
    • 组成:运行时常量池、成员变量、方法信息(vtable 虚方法表 + itable 类实现的接口的函数表)
    • 存储位置:[方法区]{.red}
    • 细节:[反射机制中的 Class 对象是依靠 Klass 对象生成的:Class 对象相当于是 Klass 对象的镜像文件]{.red}

为什么要使用 OOP-Klass 模型

  • 问题:

    • HotSpot 是采用 C++ 编写的运行 Java 程序的虚拟机
    • [那么 Java 的类如何才能够被 C++ 编写的虚拟机解析呢?]{.blue}
  • 方式:

    • 最简单的方式:为了每个 Java 类都生成对应的 C++ 类,[这意味着每个 Java 对象中也必须包含虚方法表]{.blue}

      :::info

      ① Java 中默认所有的方法都是虚方法,而 C++ 中必须使用关键字 virtual 声明方法才行

      ② Java 的虚方法的数量显然会比 C++ 中的虚方法使用更加频繁,数量更多,为每个对象都保存虚方法表显然是不明智的

      :::

    • 采用的方式:利用 OOP-Klass 模型

      • OOP 模型只用于存储对象的相关数据

      • [Klass 模型用于存储类的元数据信息,用于被 C++ 解析,]{.blue}

        [所以可以认为 Klass 对象是 C++ 中类的表现形式,和 Java 中的类相对应]{.blue}

  • 原因:[为了 Java 编写的类能够被虚拟机解析,同时也为了压缩堆空间中对象的大小]{.red}

对象大小计算

:::primary

参考博客:jvm压缩指针原理以及32g内存压缩指针失效详解

:::

  • 不同位数的虚拟机
    • 32 位虚拟机:
      • 对象头:[类元数据指针 4B + Mark Word 4B]{.blue}
      • 实例数据:[引用类型大小占据 4B,基本类型不变]{.blue}
      • 对其填充:确保对象大小为8的整数倍
    • 64 位虚拟机:
      • 对象头:[类元数据指针 8B + Mark Word 8B]{.blue}
      • 实例数据:[引用类型大小占据 8B,基本类型不变]{.blue}
      • 对其填充:确保对象大小为8的整数倍
    • 问题:
      • 64位虚拟机下的对象大小是32位虚拟机的对象的2倍,由于对齐填充,实际是1.5倍
      • [对象增大导致对象占用的堆内存更多,触发垃圾回收机制更加频繁]{.red}
      • [对象增大同时导致处理器能够缓存的对象变少,处理器缓存的命中率降低]{.red}
  • 指针压缩
    • 定义:[压缩 OOP 对象的大小]{.red}
      • 压缩对象头信息:对象头从 16B 压缩到 12B(主要就是压缩了类型指针)
      • 压缩对象引用:对象引用(不是类型指针)从 8B 压缩到 4B
      • 压缩数组类型:数组从 24B 压缩到 16B
    • 作用:[减轻堆内存的分配对象的压力]{.red}
    • 细节:JDK 6 之后默认开启指针压缩

+++danger 为什么不推荐让虚拟机内存超过 32G?

+++

对象的实例化

:::warning

前提:需要对反射机制、克隆机制、序列化机制有所了解,才能够明白这些机制是如何创建对象的

:::

对象的实例化方式图示

对象实例化方式

对象实例化方式具体实现

  • [关键字 new 创建对象]{.red}

    • 使用方式:

      public static void main(String[] args)
      {
      Object object = new Object();
      }
    • 变形:(设计模式的运用)

    • 细节:[最常见的创建对象的方式]{.blue}

  • [反射机制创建对象]{.red}

    • [直接利用反射类对象创建类对象]{.blue}

      • 使用方式:

        public class CreateObject
        {
        public static void main(String[] args) throws
        IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException
        {
        // 获得 Student 类对应的反射类对象
        Class<Student> studentClass = Student.class;
        // 利用反射类对象创建 Student 类对象
        Student student = studentClass.newInstance();
        }
        }

        class Student{
        public Student(){}
        }
      • 细节:

        • [直接利用反射类创建对象仅能够调用 公共的空参构造器创建]{.red}
        • [JDK 9 之后该方式被标记为已过时]{.green}
        反射创建对象API(1)
    • [利用反射类对象获取类的构造器创建对象]{.blue}

      • 使用方式:

        public class CreateObject
        {
        public static void main(String[] args) throws
        IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException
        {
        // 获得 Student 类对应的反射类对象
        Class<Student> studentClass = Student.class;
        // 利用反射类对象获取构造器后创建 Student 类对象
        Student student = studentClass.getConstructor().newInstance();
        }
        }

        class Student{
        // 显示声明空参构造器
        public Student(){}
        }
      • 细节:[利用构造器类创建对象可以使用有参或者无参的构造器,并且构造器的权限时任意的]{.red}

    +++ 对比这两种方法,想想为什么直接利用反射类创建对象被废弃了?

    +++

  • [克隆创建对象]{.red}

    • 使用方式

      • [浅克隆]{.blue}:

        public class CreateObject 
        {
        public static void main(String[] args) throw CloneNotSupportedException
        {
        Student student = new Student();
        Student student_clone = (Student)student.clone();
        }
        }

        // ① 一定要实现 Cloneable 接口
        class Student implements Cloneable{
        // ② 一定要重写 clone 方法:因为 Student 不是 Object 的子类
        @Override
        protected Object clone() throws CloneNotSupportedException
        {
        return super.clone();
        }
        }
      • [深克隆:本质实现还是采用的反序列化机制,就不在这里演示了]{.blue}

    • 细节:

  • [反序列化创建对象]{.red}

    • 使用方式:

      // 反序列化创建对象
      public class CreateObject
      {
      public static void main(String[] args)
      throws IOException, ClassNotFoundException
      {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      // 1.创建对象输出流
      ObjectOutputStream oos = new ObjectOutputStream(
      new BufferedOutputStream(baos));
      // 2.序列化对象
      oos.writeObject(new Student(10));
      // 3.刷新缓存,否则你是无法将对象写入流中的(血的教训)
      oos.flush();
      // 4.创建对象输入流:对象输入流必须在序列化对象之后创建,否则对象输入流会创建失败(血的教训)
      ObjectInputStream ois = new ObjectInputStream(
      new BufferedInputStream(
      new ByteArrayInputStream(
      baos.toByteArray())));
      // 5. 反序列化对象
      Student student = (Student) ois.readObject();
      // 6.测试是否创建成功
      System.out.println(student.count);
      }
      }

      // 0.序列化类必须实现序列化接口
      public class Student implements Serializable
      {
      public int count;
      public Student(int count){
      this.count = count;
      }
      }
    • 细节:[网络编程中接收传输数据创建对象最常使用的方式]{.blue}

对象的创建过程

:::primary

参考博客:Java的指针碰撞简介

:::

对象创建过程图示

对象创建过程

具体的对象创建过程

  • [对象对应的类是否已经被加载进入虚拟机内存]{.red}

    • [虚拟机定位到类在元空间运行时常量池中的符号引用]{.blue}

    • [虚拟机再判断类是否已经经历加载、连接、初始化三个过程]{.blue}

      • 如果类已经被类加载器加载,接着创建对象就行
      • 如果类没有被类加载器加载,那么虚拟机就会查找类对应的字节码文件
        • 如果查找到相应的字节码文件,就加载该类并且创建对应的 Class 对象即可
        • 如果没有查找到对应的字节码文件,那么就会抛出异常

      :::info

      ① 动态链接是将符号引用转化成直接引用的过程,实际上就是虚拟机查找类在内存中地址的过程

      ② 虚拟机能找到类的内存地址那就是加载了,找不到那就是没有加载

      :::

  • [为对象分配内存空间]{.red}

    • 堆内存规整

      • 定义:所有被对象使用过的内存存放在一边,没有被使用的内存存放在另一边
      • 分配算法:指针碰撞(Bump The Pointer)
      指针碰撞
      • 几个问题:

        +++ 为什么要叫做指针碰撞呢?指针是有了,碰撞在哪里呢?

        +++

        +++danger 如果对象超过或者小于固定的内存块大小呢?

        +++

    • 堆内存不规整

      • 定义:被对象使用的过内存和没有使用的内存交错分布
      • 分配算法:空闲列表(Free List)(类似于操作系统中的非连续分配方式)
        • 维护一个用于记录空闲内存块的列表
        • 每次都从列表中查找一个足够大的内存块分配给对象

    :::info

    ① [对象的大小在类加载的过程就已经被确定好了]{.red}:每个对象都是类的实例嘛,类的大小也就基本等同于对象的大小

    ② [对象的分配方式取决于堆内存是否规整,堆内存是否规整取决于垃圾回收器是否具有压缩整理功能]{.red}

    ③ [对象中拥有引用类型的实例变量,那么仅分配引用的空间(4字节)]{.red}

    :::

  • [确保分配内存安全]{.red}

    • 问题:[多个线程同时创建对象造成的并发问题]{.blue}
      • 前一个线程刚创建对象完毕,但是还没有来得及将引用指向对象,就切换成另外一个线程
      • 另一个线程也要创建对象,刚好和前一个线程使用同一块区域,导致其创建对象覆盖掉前一个线程创建的对象
    • 方式
      • [对分配内存空间的行为采用同步机制处理]{.blue}:虚拟机实际采用 CAS 配上失败重试的方式保证分配操作的原子性
      • [不同线程在互不干扰的区域中分配对象]{.blue}:虚拟机实际采用 TLAB 来实现分配过程的安全性
  • [初始化对象的 实例变量]{.red}:实例变量会被设置成类型 [默认值]{.red}

    :::info

    类中的类变量早在类加载阶段就已经初始化完成了,所以这里只涉及实例变量

    :::

  • [初始化对象的对象头]{.red}

  • [执行对象的构造函数]{.red}

对象的访问定位

  • 访问定位:虚拟机栈中的对象引用寻找到对象在堆空间中的实例数据的过程

  • 方式

    • 句柄访问

      • 句柄:[对象实例数据指针 + 对象类元数据信息指针]{.red}
      • 存储:堆空间中划分一块区域用于存储所有对象的句柄
      • 引用:[指向句柄在堆空间中的地址]{.red}
      • 访问过程:引用需要先找到句柄在堆空间中的位置后,再根据句柄确定对象的实例数据或者类元信息
      句柄访问
    • 直接指针访问

      • 引用:[直接指向对象在堆空间中的地址]{.red}
      • 访问过程:引用直接就可以根据地址找到对象
      • 细节:[HotSpot 虚拟机实际采用的就是直接指针访问的方式定位对象]{.blue}
      直接指针访问
  • 对比

    • 句柄访问
      • 优点:[对象在堆空间中的位置发生改变,只需要改变句柄的地址就行,对象引用的地址不需要改变]{.red}
        • 对于程序员来讲这个地址改变就是彻底不可见了,因为引用地址不会随着对象的地址而改变
        • [适用于对象地址频繁在堆空间中发生变化的情况]{.red}
      • 缺点:[每次访问对象都会增加一次寻址的开销]{.green}
      • 细节:[对象类元数据指针不再存放在对象实例数据中,也就意味着没有存放在对象头中]{.red}
    • 直接指针访问
      • 优点:[每次访问对象仅花费一次寻址的开销]{.red}
      • 缺点:[对象在堆空间中的位置发生改变,需要直接修改对象引用]{.green}
        • 对于频繁再堆空间中地址发生变化的对象是非常不利的
Author: Fuyusakaiori
Link: http://example.com/2021/09/23/jvm/runtime/heapspace/对象创建/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.