说说new一个Java对象这件事儿------Java对象创建流程
PS:本文参考的JDK21 源码版本是:jdk-21-ga
Java对象的创建流程
好多苦逼大龄程序员天天被催婚、找不到对象,但是不用愁!本文手把手教学怎么在Java里面new一个对象带回家!
在面向对象编程语言中,对象是类的实例,在编程中一个类往往会对应一个或者多个对象。因此,当我们需要使用到类中非static的成员变量和方法的时候,就需要创建对应类的对象来进行使用。
假设我们要创建一个Object类,在Java代码中我们会使用new关键字来创建Object类的一个对象:
auto
Object obj = new Object(); //创建一个Object对象,触发类加载和对象创建
如果此时Object类是首次调用,则JVM将.class字节码文件经过加载、验证、准备、解析和链接等步骤加载到JVM内存中的方法区。
接下来JVM就会开启真正的对象创建过程,依次给对象分配内存、初始化对象、设置对象头和调用构造方法,最后再返回对象的引用方便Java程序访问对象。
完整来说,Java对象的创建分为如下6个步骤:
- 类加载: 当程序运行时,JVM会将类的字节码(xxx.class文件)加载到内存中,包括加载、验证、准备、解析和链接等步骤,此时类的字节码文件会被存放在JVM内存的方法区当中。
- 分配内存: 在内存中分配对象所需的内存空间。具体的内存分配方式有很多种,包括堆上的对象分配、栈上的对象分配等,在主流的Java虚拟机中,大部分对象的内存分配发生在堆上。
- 初始化零值: 将对象的实例变量初始化为零值,数值类型的变量初始化为0,引用类型的变量初始化为null。
- 设置对象头: 对象头包含了一些用于管理对象的元信息,如哈希码、锁信息等。
- 调用构造方法: 执行对象的构造方法,进行对象的初始化。构造方法是类中用于初始化对象的特殊方法。
- 对象引用: 返回对象的引用,可以将引用赋给类变量、实例变量、局部变量等,从而使得程序可以操作这个对象。
通过上面的6个步骤,对象就成功创建了,Java代码中也拿到了对象的引用,可以通过引用去访问对象的相关属性值,下面详细介绍下对象流程当中的关键步骤,类加载流程则可以参考我前面的文章。
好的,对JDK 21源码不感兴趣的同学就可以到此为止,目前已经说清楚了Java对象创建的总体流程了。由于发现网上现存的大多数bolgs都是到此为止,个人感觉其实并没有把对象创建流程理解的很清楚,接下来我将从JDK21 的HotSpot源码角度来解析对象的创建流程。
源码分析------跟踪new关键字
创建Java类
一段朴实无华的Java代码,这里只是为了调用new关键字创建一个Java对象:
auto
package com.tsinghualei;
public class NewObj {
public NewObj() {
}
public static void main(String[] args) {
new NewObj();
}
}
java p -v 编译NewObj.class文件
这里编译Java源代码的步骤我就不赘述了,不会的可以参考我的文章:"Java大厦的基石------Java Class文件构成"
jclasslib查看字节码文件
main方法字节码如下:
auto
new #7 <com/tsinghualei/NewObj>
3 dup
4 invokespecial #9 <com/tsinghualei/NewObj.<init> : ()V>
7 astore_1
8 return
上述字节码当中的核心命令为invokespecial #9,这个命令的作用就是调用Java类的方法来完成类的初始化,JVM在执行改指令之前会执行类的初始化相关代码(假设类字节码文件已实现加载到内存,具体类加载流程可以参考我的其它文章)------Java类一生有多长------Java类的生命周期(类加载机制)。
involespecial指令对应的HotSpot代码为:hotspot/src/cpu/x86/vm/templateTable_x86.cpp,需要注意的这里是针对x86平台实现的平台相关代码,不同的平台,如arm对应的相同的文件是:hotspot/cpu/arm/templateTable_arm.cpp,其中调用的想法都是平台特有方法,这也是JVM虚拟的意义,帮我们屏蔽平台特性,实现了跨平台。
Java中new关键字入口HotSpot代码(TemplateTable::_new())
hotspot/cpu/arm/templateTable_arm.cpp 中TemplateTable::_new()方法源码,这段代码是C++语言的汇编代码,用于实现某个类的实例化操作。下面是对代码的逐行解释:
auto
// 定义TemplateTable类的_new()方法
void TemplateTable::_new() {
// 调用transition方法,参数为vtos和atos
transition(vtos, atos);
// 获取常量池中索引为1的2字节无符号整数,存储到rdx寄存器中
__ get_unsigned_2_byte_index_at_bcp(rdx, 1);
// 定义一些标签用于后续的跳转
Label slow_case;
Label slow_case_no_pop;
Label done;
Label initialize_header;
// 获取常量池和标签,存储到rcx和rax寄存器中
__ get_cpool_and_tags(rcx, rax);
// 确保即将实例化的类已经被解析
const int tags_offset = Array<u1>::base_offset_in_bytes();
__ cmpb(Address(rax, rdx, Address::times_1, tags_offset), JVM_CONSTANT_Class);
__ jcc(Assembler::notEqual, slow_case_no_pop);
// 获取InstanceKlass
__ load_resolved_klass_at_index(rcx, rcx, rdx);
// 保存klass的上下文,以初始化头部时使用
__ push(rcx);
// 确保klass已初始化并且没有finalizer
__ cmpb(Address(rcx, InstanceKlass::init_state_offset()), InstanceKlass::fully_initialized);
__ jcc(Assembler::notEqual, slow_case);
// 获取InstanceKlass中的实例大小(以字节为单位)
__ movl(rdx, Address(rcx, Klass::layout_helper_offset()));
// 检查是否有finalizer或存在其他问题
__ testl(rdx, Klass::_lh_instance_slow_path_bit);
__ jcc(Assembler::notZero, slow_case);
// 分配实例:
// 如果启用TLAB:
// 尝试在TLAB中分配
// 如果失败,进入slow path
// 初始化分配
// 退出
// 进入slow path
const Register thread = LP64_ONLY(r15_thread) NOT_LP64(rcx);
if (UseTLAB) {
NOT_LP64(__ get_thread(thread);)
__ tlab_allocate(thread, rax, rdx, 0, rcx, rbx, slow_case);
if (ZeroTLAB) {
// 字段已经被清除
__ jmp(initialize_header);
}
// 实例化前已初始化对象。如果对象大小为零,直接进入头部初始化
__ decrement(rdx, sizeof(oopDesc));
__ jcc(Assembler::zero, initialize_header);
// 初始化顶层对象字段,将rdx除以8,检查是否为奇数并测试是否为零
__ xorl(rcx, rcx);
__ shrl(rdx, LogBytesPerLong);
__ bind(initialize_header);
__ movptr(Address(rax, rdx, Address::times_8, sizeof(oopDesc) - 1*oopSize), rcx);
NOT_LP64(__ movptr(Address(rax, rdx, Address::times_8, sizeof(oopDesc) - 2*oopSize), rcx));
__ decrement(rdx);
__ jcc(Assembler::notZero, initialize_header);
// 仅初始化对象头部
__ movptr(Address(rax, oopDesc::mark_offset_in_bytes()), (intptr_t)markWord::prototype().value());
__ pop(rcx);
#ifdef _LP64
__ xorl(rsi, rsi);
__ store_klass_gap(rax, rsi);
#endif
// 触发dtrace事件以进行fastpath
{
SkipIfEqual skip_if(_masm, &DTraceAllocProbes, 0, rscratch1);
__ push(atos);
__ call_VM_leaf(
CAST_FROM_FN_PTR(address, static_cast<int (*)(oopDesc*)>(SharedRuntime::dtrace_object_alloc)), rax);
__ pop(atos);
}
__ jmp(done);
}
// slow case
__ bind(slow_case);
__ pop(rcx);
__ bind(slow_case_no_pop);
Register rarg1 = LP64_ONLY(c_rarg1) NOT_LP64(rax);
Register rarg2 = LP64_ONLY(c_rarg2) NOT_LP64(rdx);
// 获取常量池
__ get_constant_pool(rarg1);
// 获取常量池中索引为1的2字节无符号整数,存储到rarg2寄存器中
__ get_unsigned_2_byte_index_at_bcp(rarg2, 1);
// 调用InterpreterRuntime::_new方法
call_VM(rax, CAST_FROM_FN_PTR(address, InterpreterRuntime::_new), rarg1, rarg2);
__ verify_oop(rax);
// 继续执行
__ bind(done);
}
}
这段代码主要实现了一个类的实例化过程,包括对类的解析、初始化、分配内存等操作,后面我将结合HotSpot源代码来说明Java对象的创建流程。
从HotSpot源码理解------Java类创建流程
类加载
Java类加载流程一般是在Java类的静态成员变量,静态成员函数被调用或者类的对象首次创建时会被加载,具体的类加载流程可以参考文章《Java类一生有多长------Java类的生命周期(类加载机制、双亲委派机制)》。在HotSpot中的new关键字相关源码当中,在创建对象之前会先检查类是否加载,加载类的部分主要由以下几行组成:
cpp
// 获取常量池引用
__ get_cpool_and_tags(rcx, rax);
// 检查类是否已加载
const int tags_offset = Array<>::base_offset_in_bytes();
__ cmpb(Address(rax, rdx, Address::times_1, tags_offset), JVM_CONSTANT_Class);
__ jcc(Assembler::notEqual, slow_case_no_pop);
// 获取已加载的类信息
__ load_resolved_klass_at_index(rcx, rcx, rdx);
__ push(rcx); // 保存类引用
这些代码的主要作用是加载并检查类是否已经加载,具体来说:
__ get_cpool_and_tags(rcx, rax);
:获取常量池引用以及与类相关的标签信息。__ cmpb(Address(rax, rdx, Address::times_1, tags_offset), JVM_CONSTANT_Class);
:通过比较常量池中指定索引处的项的标签是否为JVM_CONSTANT_Class
来判断类是否已加载。__ jcc(Assembler::notEqual, slow_case_no_pop);
:如果类未加载,则跳转到slow_case_no_pop
标签,执行相应的慢路径逻辑。__ load_resolved_klass_at_index(rcx, rcx, rdx);
:如果类已加载,通过索引从常量池中加载已解析的类(InstanceKlass
)。__ push(rcx);
:保存加载的类引用到栈上,以备后续使用。
这部分代码是加载并检查类的逻辑,如果类已经加载,则将其引用保存在栈上供后续使用,如果类未加载,则会通过跳转到慢路径执行相应的慢路径逻辑。
分配内存(InterpreterRuntime::_new)
在LAB缓存进行快速分配
cpp
if (UseTLAB) {
NOT_LP64(__ get_thread(thread);)
__ tlab_allocate(thread, rax, rdx, 0, rcx, rbx, slow_case);
// ... (省略其他部分)
}
如果启用了TLAB(Thread-Local Allocation Buffer),则尝试在TLAB中分配内存。如果分配失败,将跳转到slow_case
标签,如果分配成功,将继续执行后续的对象初始化步骤。
慢分配
如果LAB当中快速分配失败,则会走慢路径(slow case)入口代码如下:
cpp
// 调用InterpreterRuntime::_new方法
call_VM(rax, CAST_FROM_FN_PTR(address, InterpreterRuntime::_new), rarg1, rarg2);
方法call_VM(rax, CAST_FROM_FN_PTR(address, InterpreterRuntime::_new), rarg1, rarg2);
调用 VM 中的 hotspot/share/interpreter/InterpreterRuntime::_new 方法,该方法通常用于慢路径中的对象分配,其代码如下:
auto
// 创建新对象
JRT_ENTRY(void, InterpreterRuntime::_new(JavaThread* current, ConstantPool* pool, int index))
Klass* k = pool->klass_at(index, CHECK); // 从常量池获取类信息
InstanceKlass* klass = InstanceKlass::cast(k);
// 确保不实例化抽象类
klass->check_valid_for_instantiation(true, CHECK);
// 初始化类,主要初始化类中的静态变量、静态代码块
klass->initialize(CHECK);
// 分配对象,为对象分配内存,并实际新建对象
oop obj = klass->allocate_instance(CHECK);
current->set_vm_result(obj);
JRT_END
在这段代码中,klass->initialize(CHECK);
和 oop obj = klass->allocate_instance(CHECK);
两行代码分别完成了不同的任务:
- 初始化类:
klass->initialize(CHECK);
:这一行代码是用于初始化类的阶段。在Java虚拟机中,类的初始化是一个重要的阶段,其中类的静态成员变量被赋予初始值,静态代码块被执行等。这个过程确保了在类被首次使用之前,它的状态是正确的。这一行代码可能会执行一些与类初始化相关的工作,包括静态变量的初始化等。 - 分配内存:
oop obj = klass->allocate_instance(CHECK);
:这一行代码是用于为特定的类分配一个新的对象实例。在Java中,当你通过new
关键字创建一个对象时,实际上是在内存中为该对象的实例分配内存空间。这一行代码调用了allocate_instance
方法,该方法会分配对象的内存空间并返回对象的引用。
在对象的创建过程中,通常需要先确保类已经初始化(因为对象的创建可能涉及到类的初始化过程,即类的静态成员变量,静态代码块已经执行),然后再分配对象的内存空间。这两个步骤通常是密切相关的,但在代码中可以分开来执行,以提高灵活性。
好了,对象的内存分配就解析道这里为止了,不然hotspot/share/oops/instanceKlass::allocate_instance方法跟踪下去又是很冗长的一部分内容,在这个方法里面JVM会根据Java对象的大小去堆中分配年轻代/老年代内存。这里我们只需要通过HotSpot代码知道JVM创建对象之间会去分配内存即可。
初始化零值
cpp
// 定义线程寄存器,如果是64位架构,则使用r15_thread,否则使用rcx
const Register thread = LP64_ONLY(r15_thread) NOT_LP64(rcx);
// ...(省略其他部分)
// 初始化对象的顶层字段和剩余的对象字段
__ decrement(rdx, sizeof(oopDesc));
__ jcc(Assembler::zero, initialize_header);
// 使用异或操作清零rcx寄存器,并将rdx右移LogBytesPerLong位(相当于除以2^(LogBytesPerLong))
__ xorl(rcx, rcx);
__ shrl(rdx, LogBytesPerLong);
// 初始化剩余的对象字段
{ Label loop;
__ bind(loop);
// ...(省略其他部分)
__ decrement(rdx);
__ jcc(Assembler::notZero, loop);
}
// 仅初始化对象头部
__ bind(initialize_header);
// ...(省略其他部分)
这段代码的主要作用是对对象的字段进行初始化,这个初始化过程主要是将分配好的内存区域全部置为0值,包括顶层字段和剩余的对象字段。
在初始化过程中,__ decrement(rdx, sizeof(oopDesc));
将寄存器 rdx
的值减去 sizeof(oopDesc)
的大小,然后检查是否为零。如果减去 sizeof(oopDesc)
后的值为零,说明对象的大小正好是 sizeof(oopDesc)
,也就是说这是一个空对象(没有额外字段),那么就直接跳转到 initialize_header
标签处。
如果不是,则继续执行接下来的loop,通过异或操作将rcx
寄存器清零,这一步之后,对象中所分配的内存区域全部都置为了0,这样以来对象的所有成员变量都被置为了零值。这样做的目的是清空对象的所有内存区域,方便后续的初始化MarkWord,以及对象的其它信息。
然后,将rdx
右移LogBytesPerLong
位,最后通过循环逐个初始化剩余的对象字段。最后,跳转到initialize_header
标签,就到了对象初始化的下一个步骤,设置对象头部。
设置对象头
对于设置对象头部的部分,代码是这样的:
cpp
// Initialize object header only
__ bind(initialize_header);
__ movptr(Address(rax, oopDesc::mark_offset_in_bytes()),
(intptr_t)markWord::prototype().value()); // 设置对象头部
这段代码的作用是设置对象的头部信息。在这里,__ movptr
是一个汇编指令,用于将数据从一个位置移动到另一个位置。具体来说:
-
Address(rax, oopDesc::mark_offset_in_bytes())
创建了一个地址对象,该地址指向rax
寄存器中的地址加上oopDesc::mark_offset_in_bytes()
的偏移量处。这个地址通常用于访问对象的头部信息。 -
(intptr_t)markWord::prototype().value()
计算了markWord
类的原型的数值,并将其转换为intptr_t
类型。这个数值通常包含有关对象的标记信息,例如对象的锁定状态等。 -
__ movptr
汇编指令将上述计算得到的标记信息复制到对象的头部地址中,从而设置了对象的头部信息。
这段代码的目的是将一个标记字(mark word)的数值设置到对象的头部,这个标记字包含了对象的一些元信息,如锁定状态等。这是在对象初始化过程中的一部分,用于确保对象的正确状态。
MarkWord
继续跟踪到类hotspot/share/oops/markWord.cpp的构造函数,发现其会是一个空构造函数,因此其内容在调用构造函数的时候都已经被初始化好了,都是一些默认值:
auto
// markWord 类表示 HotSpot 虚拟机对象头的标记字段
class markWord {
private:
uintptr_t _value; // markWord 的内部值
public:
// 显式构造函数,用于根据给定的值创建 markWord 实例
explicit markWord(uintptr_t value) : _value(value) {}
// 默认构造函数,默认初始化不设置 _value 的值
markWord() = default; // Doesn't initialize _value.
// 获取 markWord 的值
uintptr_t value() const { return _value; }
// 常量定义
static const int age_bits = 4; // 年龄字段所占位数
static const int lock_bits = 2; // 锁定状态字段所占位数
static const int first_unused_gap_bits = 1; // 未使用的第一个间隙的位数
static const int max_hash_bits = BitsPerWord - age_bits - lock_bits - first_unused_gap_bits;
static const int hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits; // 哈希字段所占位数
static const int second_unused_gap_bits = LP64_ONLY(1) NOT_LP64(0); // 未使用的第二个间隙的位数
static const int lock_shift = 0; // 锁定状态字段的偏移量
static const int age_shift = lock_bits + first_unused_gap_bits; // 年龄字段的偏移量
static const int hash_shift = age_shift + age_bits + second_unused_gap_bits; // 哈希字段的偏移量
// 锁定状态字段的掩码
static const uintptr_t lock_mask = right_n_bits(lock_bits);
// 锁定状态字段在原地的掩码
static const uintptr_t lock_mask_in_place = lock_mask << lock_shift;
// 年龄字段的掩码
static const uintptr_t age_mask = right_n_bits(age_bits);
// 年龄字段在原地的掩码
static const uintptr_t age_mask_in_place = age_mask << age_shift;
// 哈希字段的掩码
static const uintptr_t hash_mask = right_n_bits(hash_bits);
// 哈希字段在原地的掩码
static const uintptr_t hash_mask_in_place = hash_mask << hash_shift;
// 锁定状态字段的锁定值
static const uintptr_t locked_value = 0;
// 锁定状态字段的未锁定值
static const uintptr_t unlocked_value = 1;
// 锁定状态字段的监视器值
static const uintptr_t monitor_value = 2;
// 锁定状态字段的标记值
static const uintptr_t marked_value = 3;
// 未分配哈希值
static const uintptr_t no_hash = 0 ;
// 未分配哈希值在原地的掩码
static const uintptr_t no_hash_in_place = (address_word)no_hash << hash_shift;
// 未锁定状态字段在原地的掩码
static const uintptr_t no_lock_in_place = unlocked_value;
// 年龄字段的最大值
static const uint max_age = age_mask;
};
如上面代码所示,在Java HotSpot虚拟机中,每个对象都有一个对象头,其中的 markWord
负责存储对象的一些元信息和标记信息,主要作用如下:
- 存储标记信息: ``部分位用于标记对象的状态。常见的标记包括对象是否被锁定、对象的监视器状态、对象的标记状态等。通过这些标记,虚拟机可以了解对象的运行时状态,例如对象是否被锁定以及是否需要进行垃圾回收。
- 存储对象年龄: ``包含一个用于表示对象年龄的字段。对象年龄是为了支持Java虚拟机的分代垃圾回收机制。对象在经过一次垃圾回收后,如果存活下来,其年龄会增加,当达到一定年龄后,对象可能会被晋升到老年代。
- 存储哈希信息: ``部分位用于存储对象的哈希值。哈希值在一些场景下可以用于优化操作,例如在并发标记时,可以通过哈希值来判断对象是否已经被标记。
- 存储锁信息: 一些位用于表示对象的锁状态,包括是否被锁定、监视器锁状态等。这对于实现Java中的同步机制至关重要。
markWord
的作用是在对象头中存储一些关键的元信息,以便虚拟机在运行时能够有效地管理对象的状态、支持垃圾回收、实现同步等功能。这部分内容,对应的就是大家耳熟能祥的Java对象头MarkWord的构成,本文主要为了阐述Java对象的创建流程,因此对于MarkWord的结构图就不给出了,可以参考我的文章Java对象内存结构。
完成对象创建
cpp
__ jmp(done);
__ jmp(done);
这行代码表示无条件跳转到标签 done
处,即跳出当前代码块,结束整个逻辑流程。在前面的代码中,我们可以看到 done
标签的定义:
而在后续的代码中应该有对 done
标签进行定义和使用。在上下文中,这行跳转代码的目的是在成功创建对象后,跳转到 done
标签处,表示对象创建的整个过程已经完成。在 done
标签处可能会包含一些清理工作或者后续的逻辑。由于这部分代码片段是不完整的,具体的 done
标签的定义和使用需要在其他地方查找。
在上面的_new汇编代码当中,已经创建好的Java对象的内存地址一般不是直接返回的,而是通常是通过寄存器(如 rax
)来传递。在这里,call_VM
是一个调用虚拟机方法的宏,而 rax
寄存器用于保存返回值。以下是可能的返回对象内存地址的简化代码片段:
assembly
// 调用InterpreterRuntime::_new方法
call_VM(rax, CAST_FROM_FN_PTR(address, InterpreterRuntime::_new), rarg1, rarg2);
__ verify_oop(rax);
// 返回rax中的对象地址
这部分代码调用了 InterpreterRuntime::_new
方法,将返回值保存在 rax
寄存器中。然后,__ verify_oop(rax)
用于验证 rax
中的值是否是有效的 Java 对象引用。在实际应用中,rax
可能会作为返回值传递给调用方。
至此,JVM负责的对象创建的过程结束了,主要是加载类文件、分配内存、初始化对象、初始化MarkWord,完成上述步骤之后,会将Java对象的内存地址传递给Java层代码,后续再由Java层代码负责调用构造方法和返回引用。
总结
本文通过追踪HotSpot虚拟机的源码,详细解析了Java对象的创建流程。整个过程包括类加载、内存分配、对象初始化、MarkWord设置等关键步骤。
首先,通过类加载机制,虚拟机获取常量池引用以及相关标签信息,并检查目标类是否已加载。
接着,根据类的加载状态执行相应的逻辑,包括通过索引从常量池加载已解析的类信息,并将类引用保存在栈上。
接下来,通过InterpreterRuntime::_new方法进行内存分配,其中包括对TLAB的快速分配和慢路径分配。在慢路径分配中,进行类的初始化、对象的内存分配以及对象的初始化工作。对象的初始化过程包括零值初始化和设置对象头信息。
最后,通过返回对象的内存地址完成整个对象创建的过程。
在具体的代码分析中,重点介绍了markWord类的作用和结构,它负责存储对象的元信息、标记信息、年龄信息、哈希信息以及锁信息等。markWord的设置是对象创建过程中的关键一环,为虚拟机提供了管理对象状态、支持垃圾回收和同步操作的基础。
总体而言,深入理解Java对象创建的底层实现对于理解Java虚拟机的工作原理和性能优化具有重要意义。这篇文章通过源码解析,帮助读者更清晰地理解了Java对象创建的整个流程及关键步骤。
更多优质内容
微信公众号:ByteRaccoon、知乎\稀土掘金\小红书都叫:浣熊say
PS:本文当中有些链接跳转不过去,是因为掘金禁止跳转微信公众号的链接,对连接内容感兴趣的,可以直接看我的微信公众号原文,里面的文章可以正常跳转。