Java对象创建过程

前言

在Java开发中,我们经常使用new关键字来创建对象,但你是否想过,当执行Person person = new Person()这行代码时,JVM底层究竟发生了什么?让我们看看对象是怎么被创建的。

对象创建的六个核心步骤

1. 类加载检查

当JVM执行引擎遇到new指令时,首先会进行类加载检查:

复制代码
Person person = new Person();

JVM会执行以下检查:

  • 在常量池中定位到Person类的符号引用
  • 检查Person类是否已经被加载
  • 检查Person类是否已经被解析
  • 检查Person类是否已经被初始化

如果任何一个步骤没有完成,JVM会先执行相应的类加载过程。

2. 分配内存空间

类加载检查通过后,JVM开始为新对象分配内存。对象所需的内存大小在类加载完成后就已经确定。

2.1 两种分配方式

方式一:指针碰撞

定义:假设Java堆中内存是绝对规整的,所有使用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针为分界点的指示器,分配内存就是把那个指针向空闲空间方向挪动一段与对象大小相等的距离。

方式二:空闲列表

定义:假设Java堆中内存并不是规整的,已被使用的内存和空闲的内存相互交错,虚拟机就必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

2.2 选择依据

复制代码
if (垃圾收集器支持压缩整理) {
    使用指针碰撞方式();
} else {
    使用空闲列表方式();
}

3. 并发安全处理

对象创建是高频操作,必须保证线程安全。JVM提供两种解决方案:

3.1 CAS + 失败重试

复制代码
public Object allocateMemory(int size) {
    do {
        Object current = heapPointer.get();
        Object next = current + size;
        if (heapPointer.compareAndSet(current, next)) {
            return current;
        }
        // CAS失败,重试
    } while (true);
}

3.2 TLAB

每个线程在Java堆中预先分配一块私有内存区域,避免多线程竞争。

TLAB的优势:

  • 减少线程间同步开销
  • 提高内存分配效率
  • 支持快速的对象分配

4. 内存初始化

复制代码
public class Person {
    private String name;    // 初始化为null
    private int age;        // 初始化为0
    private boolean active; // 初始化为false
}

JVM将分配的内存空间(除对象头外)全部初始化为零值,这确保了实例字段在未显式赋值时也有确定的初始值。

各类型的零值:

数据类型 零值
boolean false
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0d
char '\u0000'
引用类型 null

5. 设置对象头

对象头是JVM管理对象的关键数据结构,包含两部分:

5.1 Mark Word

在64位JVM中,Mark Word占用8字节,存储: 对象哈希码、分代年龄、锁标志位、线程ID、时间戳和偏向锁标志。

Mark Word在不同锁状态下的存储内容:

锁状态 25bit 31bit 1bit 4bit 1bit 2bit
无锁状态 unused hashcode unused 分代年龄 0 01
偏向锁 ThreadID(54bit) Epoch(2bit) unused 分代年龄 1 01
轻量级锁 指向栈中锁记录的指针(62bit) unused 分代年龄 unused 00
重量级锁 指向互斥量的指针(62bit) unused 分代年龄 unused 10
GC标记 CMS过程用的标记信息(62bit) unused 分代年龄 unused 11

5.2 类型指针

类型指针指向对象的类元数据,JVM通过这个指针确定对象是哪个类的实例。在64位JVM中,类型指针通常占用8字节,但开启压缩指针后可压缩至4字节。

5.3 对象头示例

普通对象的对象头结构:

java 复制代码
public class Person {
    private String name;
    private int age;
}

对于上述Person对象,其对象头包含:

组成部分 大小(64位JVM) 内容描述
Mark Word 8字节 哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
类型指针 4字节(压缩)/8字节 指向Person.class的类元数据信息
对齐填充 0-7字节 确保对象大小为8字节的倍数

数组对象的对象头结构:

数组对象除了Mark Word和类型指针外,还有额外的4字节存储数组长度:

java 复制代码
int[] array = new int[10];
组成部分 大小(64位JVM) 内容描述
Mark Word 8字节 对象标记信息
类型指针 4字节(压缩)/8字节 指向int[]的类元数据
数组长度 4字节 存储数组的长度(10)
对齐填充 根据需要 保证对象大小对齐

6. 执行构造函数

复制代码
public class Person {
    private String name;
    private int age;
    
    // 编译器生成的<init>方法
    public Person(String name, int age) {
        super();           // 调用父类构造器
        this.name = name;  // 实例字段初始化
        this.age = age;    // 实例字段初始化
        // 构造函数体逻辑
    }
}

构造函数执行的详细过程:

  1. 隐式调用父类构造器 :如果没有显式调用super(),编译器会自动添加super()调用
  2. 实例字段初始化:按照在类中声明的顺序执行字段初始化
  3. 执行构造函数体:执行构造函数中的自定义逻辑
相关推荐
_r0bin_17 分钟前
前端面试准备-7
开发语言·前端·javascript·fetch·跨域·class
zhang988000019 分钟前
JavaScript 核心原理深度解析-不停留于表面的VUE等的使用!
开发语言·javascript·vue.js
硅的褶皱1 小时前
对比分析LinkedBlockingQueue和SynchronousQueue
java·并发编程
MoFe11 小时前
【.net core】天地图坐标转换为高德地图坐标(WGS84 坐标转 GCJ02 坐标)
java·前端·.netcore
季鸢2 小时前
Java设计模式之观察者模式详解
java·观察者模式·设计模式
Fanxt_Ja2 小时前
【JVM】三色标记法原理
java·开发语言·jvm·算法
蓝婷儿2 小时前
6个月Python学习计划 Day 15 - 函数式编程、高阶函数、生成器/迭代器
开发语言·python·学习
love530love2 小时前
【笔记】在 MSYS2(MINGW64)中正确安装 Rust
运维·开发语言·人工智能·windows·笔记·python·rust
Mr Aokey3 小时前
Spring MVC参数绑定终极手册:单&多参/对象/集合/JSON/文件上传精讲
java·后端·spring
slandarer3 小时前
MATLAB | 绘图复刻(十九)| 轻松拿捏 Nature Communications 绘图
开发语言·matlab