【022】JVM 运行时数据区与对象创建
2026/4/23 9:29:45 网站建设 项目流程

前面我们聊过 JVM 内存模型的基础(013 篇),这篇我们来深入细节:Class 文件长什么样?类是怎么加载的?对象是怎么创建的?内存是怎么分配的?

理解这些底层原理,能帮你:

  • 更好地理解 Java 程序的运行机制
  • 排查 ClassNotFoundException、NoSuchMethodError 等问题
  • 理解对象的内存布局,优化内存使用
  • 为学习 GC 和 JVM 调优打基础

下面我按「Class 文件结构 → 类加载过程 → 对象创建流程 → 内存分配策略」的顺序往下聊。


1. Class 文件结构 📄

1.1 Class 文件是什么?

Java 源文件(.java)经过javac编译后,生成字节码文件(.class)。JVM 加载 Class 文件,解释或编译执行。

// User.javapublicclassUser{privateLongid;privateStringname;publicvoidsayHello(){System.out.println("Hello");}}

编译后生成User.class,包含 JVM 能看懂的字节码指令

1.2 Class 文件结构

Class 文件是一种二进制文件,结构如下:

Class 文件结构: │ ├─ 魔数(Magic):0xCAFEBABE(固定) ├─ 版本号(Version):主版本 + 次版本 ├─ 常量池(Constant Pool):字面量、符号引用 ├─ 访问标志(Access Flags):public、final 等 ├─ 类索引(This Class) ├─ 父类索引(Super Class) ├─ 接口索引(Interfaces) ├─ 字段表(Fields) ├─ 方法表(Methods) └─ 属性表(Attributes)

1.3 常量池

常量池是 Class 文件中最复杂的部分,存放字面量符号引用

// 源码publicclassUser{privateStringname="Tom";}// 常量池中包含:// 1. 字符串字面量:"Tom"// 2. 类名:com/example/User// 3. 方法名:sayHello// 4. 字段名:name// 5. 字段类型:Ljava/lang/String;

1.4 方法表

每个方法包含:

  • 访问标志(public、private、static 等)
  • 方法名索引
  • 描述符(参数类型、返回值类型)
  • 属性表(包含字节码)
// 方法的字节码示例publicvoidsayHello();descriptor:()Vflags:ACC_PUBLICCode:stack=2,locals=1,args_size=10:getstatic #2// Field java/lang/System.out:Ljava/io/PrintStream;3:ldc #3// String Hello5:invokevirtual #4// Method java/io/PrintStream.println:(Ljava/lang/String;)V8:return

2. 类加载过程 🔄

2.1 类加载的五个阶段

类加载过程: │ ├─ 1. 加载(Loading) │ └─ 读取 Class 文件,生成 Class 对象 │ ├─ 2. 验证(Verification) │ └─ 验证字节码是否合法 │ ├─ 3. 准备(Preparation) │ └─ 分配内存,初始化静态变量 │ ├─ 4. 解析(Resolution) │ └─ 符号引用 → 直接引用 │ └─ 5. 初始化(Initialization) └─ 执行静态代码块,初始化静态变量

2.2 加载(Loading)

加载阶段完成三件事:

  1. 通过类的全限定名获取类的二进制字节流
  2. 将字节流转化为方法区的运行时数据结构
  3. 在堆中生成一个java.lang.Class对象,作为方法区数据的访问入口
// 加载阶段,JVM 做了什么:// 1. 读取 User.class 文件// 2. 在方法区创建类的数据结构// 3. 在堆中创建 Class 对象Class<?>clazz=Class.forName("com.example.User");

2.3 验证(Verification)

验证字节码是否安全、合法:

验证阶段作用
文件格式验证魔数、版本号是否正确
元数据验证语义分析(如父类是否是类)
字节码验证指令是否合法(如类型是否匹配)
符号引用验证能否找到引用的类/方法/字段

2.4 准备(Preparation)

准备阶段为静态变量分配内存并初始化:

publicclassUser{// 静态变量(准备阶段分配内存)publicstaticintcount=0;// 静态常量(准备阶段直接赋值)publicstaticfinalintMAX=100;// 实例变量(不在这阶段分配)privateStringname;}

注意

  • 静态变量在准备阶段分配内存
  • 静态变量在准备阶段赋默认值(0、null、false)
  • 静态常量在准备阶段赋实际值
  • 静态变量的初始值在初始化阶段赋值

2.5 解析(Resolution)

解析阶段将符号引用转换为直接引用

// 符号引用(Symbolic Reference)// "java/lang/System.out" - 一个字符串// 直接引用(Direct Reference)// 一个指针,指向方法区中的实际对象

解析的时机

  • 静态解析:类加载时解析(大部分方法、字段)
  • 动态解析:运行时解析(多态、动态代理)

2.6 初始化(Initialization)

初始化阶段执行静态代码块静态变量赋值

publicclassUser{publicstaticintcount=0;static{// 静态代码块System.out.println("User 类初始化");count=10;}}// 触发初始化:// 1. new User()// 2. 访问静态字段// 3. 调用静态方法// 4. 反射调用// 5. 子类初始化(父类先初始化)

3. 类加载器 🎯

3.1 类加载器分类

// 三层类加载器ClassLoaderloader=User.class.getClassLoader();System.out.println(loader);// AppClassLoader(应用类加载器)System.out.println(loader.getParent());// ExtClassLoader(扩展类加载器)System.out.println(loader.getParent().getParent());// BootstrapClassLoader(引导类加载器,null)
类加载器加载路径作用
Bootstrap$JAVA_HOME/lib加载 JDK 核心类库
Ext$JAVA_HOME/lib/ext加载 JDK 扩展类库
Appclasspath加载应用类

3.2 双亲委派模型

类加载器采用双亲委派机制:

加载 "java.lang.String": AppClassLoader → ExtClassLoader → BootstrapClassLoader(找到!返回) 加载 "com.example.User": AppClassLoader → ExtClassLoader → BootstrapClassLoader(找不到) → ExtClassLoader(找不到) → AppClassLoader(自己加载)

双亲委派的好处

  1. 保证类的唯一性(不会加载用户自定义的 java.lang.String)
  2. 保证安全性(核心类库不会被篡改)
  3. 避免重复加载

3.3 自定义类加载器

publicclassMyClassLoaderextendsClassLoader{@OverrideprotectedClass<?>findClass(Stringname)throwsClassNotFoundException{Stringpath="D:/classes/"+name.replace('.','/')+".class";try(InputStreamis=newFileInputStream(path);ByteArrayOutputStreambaos=newByteArrayOutputStream()){byte[]buffer=newbyte[1024];intlen;while((len=is.read(buffer))!=-1){baos.write(buffer,0,len);}returndefineClass(name,baos.toByteArray(),0,baos.size());}catch(IOExceptione){thrownewClassNotFoundException(name,e);}}}

4. 对象创建流程 🏗️

4.1 对象创建的步骤

new User() 执行流程: │ ▼ 1. 检查类是否已加载 │ ▼ 2. 分配内存 │ ▼ 3. 初始化零值 │ ▼ 4. 设置对象头 │ ▼ 5. 执行构造函数

4.1.1 检查类是否已加载

// JVM 执行的伪代码Class<?>clazz=loadClass("com.example.User");// 加载 User 类

4.1.2 分配内存

分配内存有两种方式:

// 方式 1:指针碰撞(内存连续)// 适用场景:垃圾回收器使用标记-整理算法// 原理:空闲指针移动到对象位置classPointerBumpAllocation{privatelongnextFree=100;// 空闲指针publiclongallocate(intsize){longaddr=nextFree;nextFree+=size;returnaddr;}}// 方式 2:空闲列表(内存不连续)// 适用场景:垃圾回收器使用标记-清除算法// 原理:维护一个空闲列表classFreeListAllocation{privateMap<Long,Long>freeList=newHashMap<>();publiclongallocate(intsize){// 找到合适的空闲块longaddr=findFreeBlock(size);freeList.remove(addr);returnaddr;}}

4.1.3 初始化零值

// 分配内存后,JVM 将内存清零// 所以实例变量的默认值是 0/null/falseclassUser{longid;// 0Stringname;// nullbooleanactive;// false}

4.1.4 设置对象头

对象头包含:

对象头: │ ├─ Mark Word:哈希码、GC 年龄、锁状态 ├─ Class Pointer:指向方法区的类元数据 └─ Array Length:(数组才有)数组长度
// 对象头信息classObjectHeader{// Mark Word(32/64 位)// 存储:哈希码、GC 分代年龄、锁状态// Class Pointer// 指向方法区中的 Class 对象// Array Length(数组)// 数组长度}

4.1.5 执行构造函数

// 最后调用构造函数classUser{privateLongid;privateStringname;publicUser(Longid,Stringname){this.id=id;this.name=name;}}// JVM 执行:// 1. 调用父类构造函数// 2. 初始化实例变量// 3. 执行构造函数体

5. 对象内存布局 📊

5.1 对象内存布局三部分

对象在堆中的布局: │ ├─ 对象头(Header) │ ├─ Mark Word:8 字节(64 位 JVM) │ ├─ Class Pointer:8 字节(64 位 JVM,开启压缩 4 字节) │ └─ Array Length:4 字节(数组才有) │ ├─ 实例数据(Instance Data) │ ├─ 父类字段 │ └─ 子类字段 │ └─ 对齐填充(Padding) └─ 8 字节对齐

5.2 对象头详解

Mark Word(64 位 JVM):

状态内容
无锁哈希码(25) + 分代年龄(4) + 偏向锁(1) + 锁标志(2)
偏向锁线程ID(23) + Epoch(2) + 分代年龄(4) + 偏向锁(1) + 锁标志(2)
轻量级锁指向栈中锁记录的指针(30) + 锁标志(2)
重量级锁指向Monitor的指针(30) + 锁标志(2)
GC标记空(30) + 锁标志(2)

5.3 实例数据

字段排列顺序

classParent{longa;// 8 字节intb;// 4 字节}classChildextendsParent{intc;// 4 字节Objectd;// 8 字节(64 位)}// JVM 优化:相同宽度的字段放一起// 实际排列:a(8) + c(4) + padding(4) + d(8) + b(4) + padding(4) = 32 字节

5.4 对齐填充

JVM 要求对象起始地址是8 的倍数

// 实例数据 22 字节 → padding 6 → 28 字节// 实例数据 24 字节 → 无需 padding

6. 内存分配策略 💡

6.1 对象优先在 Eden 分配

// 大多数对象在 Eden 区分配Useruser=newUser();// 在 Eden 区

6.2 大对象直接进入老年代

// 大对象(超过阈值)直接进入老年代byte[]large=newbyte[10*1024*1024];// 10MB,大对象

阈值配置

# 默认阈值:eden 区的一半-XX:PretenureSizeThreshold=1m

6.3 长期存活对象进入老年代

// 经历 15 次 Minor GC 后,进入老年代// 年龄阈值:-XX:MaxTenuringThreshold=15

6.4 动态对象年龄判断

// 如果 Survivor 区中相同年龄的所有对象大小之和 > Survivor 区的一半// 年龄 >= 该年龄的对象直接进入老年代

7. 常见问题与排查 🔍

7.1 ClassNotFoundException

原因:类加载器找不到 Class 文件

排查

# 检查 classpath 是否正确java-cp.:lib/* MyApp# 检查 jar 包是否包含类jar-tfapp.jar|grepUser

7.2 NoSuchMethodError

原因:类的方法签名不匹配

排查

# 检查方法签名javap-pcom.example.User

7.3 内存分配失败

原因:堆内存不足

排查

# 调整堆大小java-Xms512m-Xmx2gMyApp

小结

  • Class 文件包含魔数、版本号、常量池、字段表、方法表等
  • 类加载分为加载、验证、准备、解析、初始化五个阶段
  • 双亲委派保证类的唯一性和安全性
  • 对象创建流程:检查类 → 分配内存 → 初始化零值 → 设置对象头 → 执行构造函数
  • 对象内存布局包括对象头、实例数据、对齐填充三部分
  • 内存分配策略:对象优先在 Eden、大对象直接进老年代、长期存活对象晋升老年代

下一篇(023)预告:GC 入门:分代、常见收集器名词、如何读 GC 日志——Minor GC、Full GC、串行收集器、并行收集器、CMS、G1。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询