一、 当 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

1546

被折叠的 条评论
为什么被折叠?



