Java的加载过程与内存模型
Java的加载过程与内存模型
Java加载组件
编译时
进行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() {} |
被调用时执行 | 存储在元空间 | 不依赖对象即可调用的方法 |
类模板在内存中的布局
变量,引用,类型和对象
通过上面的分析,我们可以看出 :
Java 文件中声明的变量名,本质上是对对象的引用标识。在编译和加载过程中,它们会被转化为对具体内存位置的间接访问方式:
静态变量会在类加载后映射为元空间中的字段引用;
局部变量则被分配到局部变量表中的索引(slot),在运行时与具体对象地址建立关联。
这使我们可以重新理解 Java 的多态定义:父类类型的引用可以指向子类对象。编译器仅根据引用的静态类型进行方法合法性校验,而对象在运行时的实际类型仍然是子类。当调用实例方法时,JVM 会根据对象实际类型查找其虚方法表(vtable),以确定最终调用的具体方法实现。父类引用的作用仅在于提供一种统一的行为接口,而不关注行为的具体实现。
所以 ,在java中 :
变量名(引用) → 编译,加载后被编译为直接引用或局部变量表中的索引
类型 → 变量的声明类型决定了其可用的操作(行为接口),而对象的实际类型由右侧的实例化过程决定,并影响运行时的动态方法分派。
对象 → 对象的存储和行为依赖于修饰词声明和对象的具体创建过程,会分布在
- 代码(常量数值直接嵌入)
- 常量池(static final 修饰的非数值类型)
- 元空间(静态变量)
- 堆(new 或反射构建的对象)
- 栈(局部变量)