jvm对象创建及对象的内存布局

一、 当 JVM 遇到 new 指令时,首先检查该指令的参数(即类符号引用)是否能在常量池中定位到一个类的符号引用.并检查这个类是否已被加载、解析和初始化过。如果没有,则必须先执行相应的类加载过程。

按 JVM 执行顺序:案例

步骤 1:在常量池中定位符号引用
案例:Person p = new Person(); 在代码片断中: 0: new #2 实际上是:“请创建 Person 类的一个实例”,而 #2 是对 Person 类的 符号引用(Symbolic Reference)
public class Person {
private String name;
public Person() {
this.name = “Unknown”;
}
}
// Main.java
public class Main {
public static void main(String[] args) {
Person p = new Person(); // 这一行触发 new 指令
}
}

(a) javap -v Main, 可以看到如下片断

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class Person 表示 创建一个新对象, #2 是一个 常量池索引
3: dup
4: invokespecial #3 // Method Person.“”()V
7: astore_1
8: return

(b) 在常量池中,第一列表示类型#2 = Class,第二列就是值#14,找到 Person这个类名符号. 这个过程就是:找到了类的符号引用。

Constant pool:
#1 = Methodref #4.#13 // java/lang/Object.“”()V
#2 = Class #14 // Person 就是这里
#3 = Methodref #2.#15 // Person.“”()V
#4 = Class #16 // java/lang/Object
#14 = Utf8 Person

步骤 2:检查该类是否已被加载、解析、初始化

JVM 查询 方法区(元空间) 中是否存在 Person 类的 Class 对象(即已加载的类元数据):
(a) 如果方法区不存在,即初始状态, 触发 类加载过程
(b) 如果方法区存在, 直接跳过加载,继续分配内存

(1)加载(Loading)作用:将.class文件存放到方法区中
从 .class 文件(或网络、数据库等)读取二进制流
转换为方法区中的 Class 对象(如 java.lang.Class)
在运行时常量池中注册该类的直接引用(比如把 #2 解析为指向 Person 的 Class 对象指针)

(2)链接(Linking)作用:给变量设置默认值及将常量池符号引用变成内存地址
验证(Verification):确保字节码安全(如无非法跳转)
准备(Preparation):为类变量(static 字段)分配内存并设默认值(如 int x = 0,不是 1)
解析(Resolution):将常量池中的符号引用(如 #2 Person)替换为 直接引用(内存地址)
此时,new #2 中的 #2 已被解析为 Person 类在方法区的真实地址!
(3)初始化(Initialization)
执行 方法(静态代码块 + 静态变量赋值)

步骤 3:分配内存 & 构造对象
类加载完成后,JVM 才真正执行:

  • 在 Java 堆 中为 Person 对象分配内存(大小在类加载时已确定)
  • 将内存初始化为零值(如 name = null)
  • 设置对象头(Mark Word + Klass Pointer):它是对象运行时的状态无法通过class看出来,存储着运行时对象的元数据如hashCode, GC分代年龄,锁状态,线程id
  • 对象静态代码及成员变量初始完成后,对象也有了,此时再执行构造方法,按程序员的意愿初始化对象成员信息。
    执行 invokespecial #3 → 调用 Person.() 构造方法 在构造方法中:this.name = “Unknown”;
  • 上面 构造器,完成程序员需要的对象信息之后对象真正构造出来,将对象引用存入局部变量 p

jvm加载流程:
main() 执行 new Person()

JVM 解析 new #2 → 查常量池 #2 = Class Person

检查方法区:Person 类是否存在?
├─ 否 → 触发类加载(加载 → 链接 → 初始化)
└─ 是 → 跳过

类加载完成 → 符号引用 #2 解析为直接引用(真实地址)

在堆中分配内存 + 初始化零值 + 设置对象头

调用 构造方法(设置 name = “Unknown”)

返回对象引用 → p = …

问题:

A, 为了保证jvm在分配对象内存时指针的原子性, 会有一个(Thread Local Allocation Buffer, TLAB)每个线程各自分配自己的对象内存, 它需要-XX:+/-UseTLAB手动开启吗?
答:默认是开启的。 通过java -XX:+PrintFlagsFinal -version | grep UseTLAB 可以打印出来。 其中 -XX:-UseTLAB 这表示关闭,不建议关闭

B.() 是构造器吗?
是!它是构造器在字节码中的真实名称
C.new 和 顺序?
答:new → 分配内存(即在堆中分配内存 + 初始化零值 + 设置对象头)
→ invokespecial 执行构造逻辑(完成程序员需要的对象信息之后对象真正构造出来)

二、对象的内存布局:对象在堆内存中的布局分成三部分,对象头(对象的元数据如hashCode, GC分代年龄,锁状态,线程id)、实例数据、对齐填充(jvm自动内存管理要求任何对象大小都是8字节的整数倍,如果实例达不到,就要对齐补全)

问题A:不管在32位还是64位jvm,都是在最低2位bit来表示什么状态呢?

锁状态lock bits (最低2位)说明
无锁(Normal)01对象未被锁定
偏向锁(Biased)01需结合第 3 位(biased_lock bit)区分
轻量级锁(Lightweight)00指向栈中 Lock Record
重量级锁(Heavyweight)10指向 ObjectMonitor
GC 标记(Marked for GC)11对象被 GC 标记

注意:偏向锁和无锁都使用 01,此时需看 第 3 位(biased_lock bit):
biased_lock = 1 → 偏向锁
biased_lock = 0 → 无锁

在64bit虚拟机中对象头的分布情况:

| unused:25bit | hash:31bit | age:4bit | biased_lock:1bit | lock:2bit |
^ ^ ^ ^ ^
高25位空闲 哈希码 分代年龄 偏向锁标志 最低2位(锁状态)

问题B: 在jvm中实例数据,默认会将相同宽度的字段放在一起是吧?父类的定义的变量初始化总是在子类之前?

答:是的。 JVM 将所有字段视为一个集合,按继承顺序分组(父类组 → 子类组),然后在每组内按宽度降序重排(8→4→2→1)即long,double最先放一起,最终合并为连续内存块。

AI的langchain,langgraph , java,spring,hadoop, flink流式计算, 心理学,哲学相关知识探讨.
本人邮箱:luyllyl@163.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值