JVM 类加载

JVM 类加载

一. 类文件结构

一个简单的 HelloWorld.java

复制代码
package cn.itcast.jvm.t5;
​
// HelloWorld 示例
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

执行 javac -parameters -d . HelloWorld.java 就能得到字节码文件. (其中 -parameters 表示"让编译器在生成的字节码中记录方法参数的名称信息").

1. 类文件结构规范
复制代码
ClassFile {
    u4              magic;                                   # 魔数 (标识文件是否为有效的类文件)
    u2              minor_version;                           # 次版本号
    u2              major_version;                           # 主版本号   
    u2              constant_pool_count;                     # 常量池长度
    cp_info         constant_pool[constant_pool_count-1];    # 常量池
    u2              access_flags;                            # 访问标志 (public/private/final/abstract)
    u2              this_class;                              # 当前类
    u2              super_class;                             # 父类
    u2              interfaces_count;                        # 接口数量
    u2              interfaces[interfaces_count];            # 接口 
    u2              fields_count;                            # 字段数量
    field_info      fields[fields_count];                    # 字段
    u2              methods_count;                           # 方法数量
    method_info     methods[methods_count];                  # 方法
    u2              attributes_count;                        # 类的附加属性数量
    attribute_info  attributes[attributes_count];            # 类的附加属性
}
2. 部分字段说明
  1. 魔数

0~3字节 (共占4个字节) 表示当前文件是否为 class 类型的文件 (不同类型的文件有不同的魔数信息).

  • ca fe ba be 表示当前文件时 .class 文件.
  1. 版本

4~7字节 (共占4个字节) 表示类的版本.

  • 00 00 00 34 (十进制52) 表示 Java8.

例子: 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

  1. 常量池
Constant Type Value
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

8~9字节 (共占4个字节) 表示常量池长度. 例如: 00 23 (十进制35) 表示常量池中有34项 (#1~#34项).

从10字节开始往后就是常量池.

字节码文件示例:

复制代码
ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09 
00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07 
00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 
56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e 
75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63 
61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01 
00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63 
61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f 
57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16 
28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 
69 6e 67 3b 29 56 01 00 04 69 72 67 73 01 00 13 
5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 
6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61 
6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46 
69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64 
2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e 
00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64 
07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74 
63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 
6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61 
6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61 
2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f 
75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72 
69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76 
61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 
01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a 
61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 
29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 
00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01 
00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00 
00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00 
01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00 
0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00 
09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a 
00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b 
00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00 
00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00 
00 00 02 00 14

二. 类加载阶段

1. 加载
(1) 核心任务

将类的字节码文件 (.class文件) 加载到 JVM 内存中, 并生成一个代表该类的 java.lang.Class 对象.

(2) 具体操作
  • 通过类的全限定名 (如com.example.User) 找到对应的字节码文件.

  • 将字节码文件的二进制数据读入 JVM 内存.

  • 在方法区中创建该类的运行时数据结构 (包含 类的版本, 字段, 方法等信息).

  • 在堆中生成一个 Class 类的实例, 作为方法区中该类数据的访问入口.

注意

  • 加载阶段由类加载器 (ClassLoader) 完成.

  • 加载阶段与连接阶段的部分操作可能重叠 (如字节码验证可能在加载未完成时就开始).

2. 连接
2.1 验证

验证字节码是否符合 JVM 规范, 避免不合规范的字节码危害 JVM 安全.

主要验证内容:

  1. 文件格式验证 (检查是否符合类文件规范 如 魔数, 版本号 是否正确).

  2. 元数据验证 (检查类的元数据信息 如 访问修饰符, 类中字段和方法 是否符合语法规则).

  3. 字节码验证 (检查方法体中的字节码指令是否合法).

  4. 符号引用验证 (检查常量池中的符号是否能被正确解析).

2.2 准备

为类的 静态变量 (类变量) 分配内存, 并设置默认初始值.

:

  • 默认初始值并非代码中定义的初始值, 而是 Java 的 "零值" (如 int 为 0, boolean 为 false, 引用类型为 null).

  • 被 final 修饰的 基本类型 或 字符串类型 赋值操作会在准备阶段完成.

2.3 解析

将常量池中的 符号引用 转换为 直接引用.

  • 符号引用: 用字符串描述的引用 (如类名, 方法名, 字段名), 存在于字节码的常量池中 (在编译期生成).

  • 直接引用: 指向内存中实际地址的引用 .

解析阶段并非必须按顺序执行, JVM 可在执行字节码指令时动态触发解析 (延迟解析).

3. 初始化
(1) 核心任务

执行*++类的初始化代码++* (执行 静态变量赋值 和 静态代码块), 将类变量设置为开发者定义的初始值.

(2) 触发时机

初始化阶段是类加载的最后一步, 只有在 主动使用类 时才会触发初始化 (是惰性的).

常见触发时机如下:

  • 创建类的实例 (如 new User()).

  • 调用类的静态方法.

  • 访问类的静态变量 (非final).

  • 反射调用 (如 Class.forName("com.example.User") )

  • 初始化子类时, 其父类会先被初始化.

(3) 执行顺序

  • 父类的初始化代码先于子类执行.

  • 静态变量的赋值与静态代码块按代码定义顺序执行.

注: 不会触发初始化的情况

  • 访问类的 static final 静态常量 (基本类型和字符串) 不会触发初始化.

  • 访问类对象 .class 不会触发初始化.

  • 创建该类的数组不会触发初始化.

  • 类加载器的 loadClass() 方法.

  • Class.forName 的参数 2 为 false 时.

三. 类加载器

以 JDK 8 为例, 类加载器的层级关系如下:

名称 加载哪里的类 说明
Bootstrap ClassLoader (启动类加载器) JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader (扩展类加载器) JAVA_HOME/jre/lib/ext 上级为 Bootstrap, 显示为 null
Application ClassLoader (应用程序类加载器) classpath 上级为 Extension
自定义类加载器 自定义 上级为 Application
1. 启动类加载器

("Bootstrap ClassLoader")

是 Java 类加载层次结构中的顶层类加载器

  • 加载范围 : 负责加载 Java 运行时核心类库, 主要加载 %JAVA_HOME%/jre/lib 目录下的类库文件 (如 java.lang.Object, java.util.List 等). 这些类库是 Java 运行的基础, 提供了最核心的功能.

  • 实现方式 : 由 ++C++ 语言实现++, 是JVM的一部分.

    在 Java 程序中无法直接访问. 如果在 Java 代码中++调用某个类的getClassLoader()方法, 得到的返回值为 null, 就说明这个类是由启动类加载器加载的++.

  • 作用 : 加载 Java 核心类库, 为 Java 程序提供最基础的类加载支持, 保证 JVM 能够运行最基本的 Java 代码.

2. 扩展类加载器

("Extension ClassLoader")

扩展类加载器 的父加载器是 启动类加载器

  • 加载范围 : 负责加载 Java 的扩展类库, 主要加载 %JAVA_HOME%/jre/lib/ext 目录下 和 由 java.ext.dirs 系统属性指定的目录中的类库文件. 这些扩展类库可以用于扩展 Java 的功能, 例如一些第三方提供的与系统交互相关的扩展包等.

  • 实现方式 : 由 ++Java 语言实现++, 是 java.lang.ClassLoader类的子类.

    如果在 Java 代码中++调用某个类的getClassLoader()方法, 得到的返回值为 扩展类加载器的实例, 就说明这个类是由扩展类加载器加载的++.

  • 作用 : 加载扩展目录下的类库, 为 Java 程序提供额外的功能支持, 且加载的类可以被应用程序类加载器所加载的类访问.

3. 应用程序类加载器

("Application ClassLoader")

应用程序类加载器 的父加载器是 扩展类加载器.

  • 加载范围 : 负责加载 应用程序的类路径 (classpath) 下的所有类, 包括开发者自己编写的类以及引用的第三方类库 (比如通过Maven / Gradle 引入的依赖). 开发中, 我们编写的业务代码, 配置文件等都是由应用程序类加载器来加载的.

  • 实现方式 : 由 ++Java 语言实现++, 是java.lang.ClassLoader 类的子类.

    如果在 Java 代码中++调用某个类的getClassLoader()方法, 得到的返回值为 应用程序类加载器的实例, 就说明这个类是由应用程序类加载器加载的++.

  • 作用 : 是 Java 应用程序中最常用的类加载器, 负责加载应用程序运行所需的各类业务逻辑类依赖类库, 让应用程序能够正常运行.

4.双亲委派模型

这三种类加载器遵循 双亲委派模型 : 即当一个类加载器收到类加载请求时, 它 ++不会尝试自己去先加载这个类,而是把请求委托给父类加载器去完成, 依次向上, 直到顶层的启动类加载器++. 只有当父类加载器无法完成加载任务时 (即: 父类加载器在它的加载范围内找不到需要加载的类), 子类加载器才会尝试自己去加载这个类.

双亲委派模型的优点:

  1. 避免类的重复加载: 比如 java.lang.Object 类, 无论由哪个类加载器加载, 最终都是由启动类加载器加载, 不会出现多个不同版本的 Object 类.

  2. 保证 Java 核心类库的安全性: 核心类库都是由启动类加载器加载, 不可被篡改和替换. 防止了恶意代码替换 Java 核心类造成安全问题.

相关推荐
野犬寒鸦2 小时前
今日面试之项目拷打:锁与事务的深度解析
java·服务器·数据库·后端
sunbin2 小时前
软件授权管理系统-整体业务流程图
后端
_OP_CHEN2 小时前
C++:(四)类和对象(中)—— 构造、析构与重载
开发语言·c++·类和对象·构造函数·析构函数·运算符重载·日期类
间彧2 小时前
Java中,wait()和sleep()区别
后端
爱读源码的大都督3 小时前
Spring AI Alibaba JManus底层实现剖析
java·人工智能·后端
间彧3 小时前
synchronized的wait/notify机制详解与实战应用
后端
努力的小雨3 小时前
CodeBuddy CLI工具深度测评:从零到一实现鸿蒙游戏开发实践
后端
文心快码BaiduComate3 小时前
北京互联网大会 | 百度副总裁陈洋:AI Coding为新质生产力注入“新码力”
前端·后端·程序员
MediaTea3 小时前
Python 编辑器:IDLE
开发语言·python·编辑器