Java的加载过程与内存模型

Java的加载过程与内存模型

Java加载组件

image

编译时

进行java文件的编译,最终转换为字节码文件

类加载时

  • 类加载器(ClassLoader):负责加载类文件
  • 加载(Loading):读取类文件并创建二进制表示
  • 连接(Linking):分为三个子步骤
    • 验证:确保类文件结构正确
    • 准备:为静态变量分配内存并设置初始值
    • 解析:将符号引用转换为直接引用
  • 初始化(Initialization):执行类的静态初始化代码
  • 使用(Using):程序中使用类
  • 卸载(Unload):类被JVM卸载

运行时

  • JVM运行时内存区:
    • 元空间(Metaspace):存储类信息、常量和静态变量
    • 堆(Heap):存储对象实例和数组
    • 虚拟机栈(JVM Stack):包含方法调用的栈帧和局部变量表
    • 程序计数器(PC Register):记录当前执行的指令地址
  • 执行引擎(Execution Engine):
    • 解释器(Interpreter):逐条解释执行字节码
    • JIT编译器:将热点代码编译为本地机器码,提高执行效率
    • 垃圾回收器(GC):自动管理内存,回收不再使用的对象
    • 本地方法接口(JNI):调用本地方法
    • 本地方法库:C/C++实现的方法和操作系统API
  • 执行过程:
    • 加载类的字节码
    • 解析字节码
    • 根据代码热度决定使用解释执行或JIT编译
    • 执行生成的机器码
    • 必要时调用系统API
    • 自动进行内存管理和垃圾回收

Java代码块的存储位置和加载流程

类加载流程

编译时

将 Java 源文件编译为字节码文件(.class),期间编译器为每个方法建立局部变量表,
使用插槽(slot)索引来管理变量引用和原始类型的存储。

编译器根据方法类型(静态/成员方法)和变量来源(this、参数、局部变量)决定变量在局部变量表中的排布顺序。

类加载

类加载器将 class 文件加载到 JVM,并在元空间中构建 class 元信息(如 instanceKlass)
将字段、方法、常量池、接口等信息组织到元空间结构中。

静态方法、final 方法、private 方法在加载时即可确定其调用目标,无需在运行时通过虚方法表进行分派。

JVM 构建字段布局信息(field layout),根据字段描述符计算字段在类和对象中的偏移地址。

同时将类常量池中的符号引用解析为直接引用(符号引用 → 具体类、字段或方法的内存地址)。

运行时

当类首次被主动使用(如 new 操作)时,如果尚未初始化,则触发 <clinit> 静态初始化流程。

JVM 执行静态变量初始化和静态代码块,初始化类的元数据。

实例化时,JVM 查询该类实例所需的内存布局,并向堆内存申请对应大小空间。

JVM 在执行构造函数前,会先执行成员变量的初始化表达式和实例初始化块。

这些初始化代码由编译器编译时插入到 <init> 构造方法的开头。

构造函数执行时,按局部变量表加载 this 引用和方法参数,进入对象初始化流程。

JVM 根据字段描述符数组的偏移量以及继承结构,为每个成员变量分配偏移地址,按偏移将数据写入堆中对象实例。

构造函数执行完成后,将对象的引用(oop 指针)返回给调用者,通常压入操作数栈以供后续使用。

调用实例方法时,JVM 通过对象中的类元数据指针访问虚方法表(vtable),
按方法签名找到对应方法的偏移并执行。

static,final,private方法 在前期进行了静态绑定,不需要查询虚方法表,通过静态绑定(确定方法地址)执行

Java成员组件的存储位置和执行时间点

类型 定义方式 执行时间点 存储位置 描述
静态常量 static final 修饰字段 编译期内联 或 类加载时 字节码中为常量池引用;可能内联进调用类 编译器常将其内联优化,若为编译期常量,不依赖类初始化
静态变量 static 修饰的字段 类加载时,初始化阶段 存储在元空间 所有对象共享;存在于元空间中
静态初始化块 static { ... } 类加载时,静态变量之后 存储在元空间 通常用于复杂的静态初始化逻辑
实例变量 普通字段(无 static 对象创建时,构造器之前 存储在堆上 每个对象独立拥有
初始化块 { ... } (非 static) 对象创建时,实例变量之后、构造器之前 存储在堆上 主要用于多个构造器共享的初始化代码
构造器 ClassName(...) { ... } 对象创建时,初始化块之后 存储在元空间 实例构造函数,用于完成对象的最终初始化
方法(成员方法) public void method() { ... } 被调用时执行 存储在元空间 普通成员函数
静态方法 public static void method() {} 被调用时执行 存储在元空间 不依赖对象即可调用的方法

类模板在内存中的布局

image

变量,引用,类型和对象

通过上面的分析,我们可以看出 :

Java 文件中声明的变量名,本质上是对对象的引用标识。在编译和加载过程中,它们会被转化为对具体内存位置的间接访问方式:
静态变量会在类加载后映射为元空间中的字段引用;
局部变量则被分配到局部变量表中的索引(slot),在运行时与具体对象地址建立关联。

这使我们可以重新理解 Java 的多态定义:父类类型的引用可以指向子类对象。编译器仅根据引用的静态类型进行方法合法性校验,而对象在运行时的实际类型仍然是子类。当调用实例方法时,JVM 会根据对象实际类型查找其虚方法表(vtable),以确定最终调用的具体方法实现。父类引用的作用仅在于提供一种统一的行为接口,而不关注行为的具体实现。

所以 ,在java中 :

变量名(引用) → 编译,加载后被编译为直接引用或局部变量表中的索引

类型 → 变量的声明类型决定了其可用的操作(行为接口),而对象的实际类型由右侧的实例化过程决定,并影响运行时的动态方法分派。

对象 → 对象的存储和行为依赖于修饰词声明和对象的具体创建过程,会分布在

  • 代码(常量数值直接嵌入)
  • 常量池(static final 修饰的非数值类型)
  • 元空间(静态变量)
  • 堆(new 或反射构建的对象)
  • 栈(局部变量)

Java的加载过程与内存模型
http://gadoid.io/2025/04/12/Java的加载过程与内存模型/
作者
Codfish
发布于
2025年4月12日
许可协议