对象创建
对象的组成部分
对象头(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}
- 各个不同的锁不在此细讲,因为涉及到并发编程
[类型指针]{.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; |
测试代码
public static void main(String[] args) |
测试结果

OOP-Klass 模型
:::primary
参考博客:
【理解HotSpot虚拟机】对象在jvm中的表示: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
:::
- 不同位数的虚拟机
- 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}
- 32 位虚拟机:
- 指针压缩
- 定义:[压缩 OOP 对象的大小]{.red}
- 压缩对象头信息:对象头从 16B 压缩到 12B(主要就是压缩了类型指针)
- 压缩对象引用:对象引用(不是类型指针)从 8B 压缩到 4B
- 压缩数组类型:数组从 24B 压缩到 16B
- 作用:[减轻堆内存的分配对象的压力]{.red}
- 细节:JDK 6 之后默认开启指针压缩
- 定义:[压缩 OOP 对象的大小]{.red}
+++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}
[利用反射类对象获取类的构造器创建对象]{.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 的子类
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 来实现分配过程的安全性
- 问题:[多个线程同时创建对象造成的并发问题]{.blue}
[初始化对象的 实例变量]{.red}:实例变量会被设置成类型 [默认值]{.red}
:::info
类中的类变量早在类加载阶段就已经初始化完成了,所以这里只涉及实例变量
:::
[初始化对象的对象头]{.red}
[执行对象的构造函数]{.red}
对象的访问定位
访问定位:虚拟机栈中的对象引用寻找到对象在堆空间中的实例数据的过程
方式
句柄访问
- 句柄:[对象实例数据指针 + 对象类元数据信息指针]{.red}
- 存储:堆空间中划分一块区域用于存储所有对象的句柄
- 引用:[指向句柄在堆空间中的地址]{.red}
- 访问过程:引用需要先找到句柄在堆空间中的位置后,再根据句柄确定对象的实例数据或者类元信息
直接指针访问
- 引用:[直接指向对象在堆空间中的地址]{.red}
- 访问过程:引用直接就可以根据地址找到对象
- 细节:[HotSpot 虚拟机实际采用的就是直接指针访问的方式定位对象]{.blue}
对比
- 句柄访问
- 优点:[对象在堆空间中的位置发生改变,只需要改变句柄的地址就行,对象引用的地址不需要改变]{.red}
- 对于程序员来讲这个地址改变就是彻底不可见了,因为引用地址不会随着对象的地址而改变
- [适用于对象地址频繁在堆空间中发生变化的情况]{.red}
- 缺点:[每次访问对象都会增加一次寻址的开销]{.green}
- 细节:[对象类元数据指针不再存放在对象实例数据中,也就意味着没有存放在对象头中]{.red}
- 优点:[对象在堆空间中的位置发生改变,只需要改变句柄的地址就行,对象引用的地址不需要改变]{.red}
- 直接指针访问
- 优点:[每次访问对象仅花费一次寻址的开销]{.red}
- 缺点:[对象在堆空间中的位置发生改变,需要直接修改对象引用]{.green}
- 对于频繁再堆空间中地址发生变化的对象是非常不利的
- 句柄访问