类加载的过程
Java虚拟机中类加载的全过程,大概可分为加载、验证、准备、解析、初始化这五个阶段。
1.加载
在加载阶段,Java虚拟机大概需要完成三件时区:
- 通过一个类的全限定名来获取定义此类的二进制流
- 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
加载阶段既可以使用Java虚拟机里内置的引导类加 载器来完成,也可以由用户自定义的类加载器去完成,开发人员通过定义自己的类加载器去控制字节 流的获取方式(重写一个类加载器的findClass()或loadClass()方法),实现根据自己的想法来赋予应用 程序获取运行代码的动态性。
对于数组类而言,情况就有所不同,数组类本身不通过类加载器创建,它是由Java虚拟机直接在内存中动态构造出来的。数组类的创建规则遵循以下原则:
- 如果数组的组件类型是引用类型,那就递归去加载这个组件类型,数组类将被标 识在加载该组件类型的类加载器的类名称空间上
- 如果数组的组件类型不是引用类型(例如int[]数组的组件类型为int),Java虚拟机将会把数组C标记为与引导类加载器关联。
- 数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的 可访问性将默认为public,可被所有的类和接口访问到。
2.验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》,验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。
1.文件格式验证
第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这一阶
段可能包括下面这些验证点:
-
是否以魔数0xCAFEBABE开头。
-
主、次版本号是否在当前Java虚拟机接受范围之内。
-
常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
-
指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
-
CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据。
-
Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
2.元数据验证
第二阶段是对字节码描述的信息进行语义分析
-
这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
-
这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
-
如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
-
类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。
-
......
3.字节码验证
字节码验证的主要目的是通过数据流分析和控制流分析,确定程序予以是合法的、符合逻辑的,该阶段会对类的方法体进行校验,保证方法在运行时不会危害虚拟机安全,例如:
- 保证操作数栈的数据类型和指令代码序列能配合工作
- 保证任何跳转指令都不会跳转到方法体意外的字节码指令上
- 保证发放提中的类型转换是有效的,例如可以把一个子类对象复制给父类数据类型
- ......
4.符号引用验证
符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段发生。主要验证内容:
- 符号引用中通过字符串描述的权限丁名是否能找到对应的类
- 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段
- 符号引用中的类、字段、方法的可访问性是否可被当前类访问
- ......
符号引用验证的主要目的是确保解析行为能正常执行,如果无法通过符号引用验证,Java虚拟机将会抛出一个java.lang.IncompatibleClassChangeError
的子类异常,如:java.lang.IllegalAccessError
、java.lang.NoSuchFieldError
、java.lang.NoSuchMethodError
等。
3.准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。此时进行内存分配的仅包括类变量,不包括实例变量,实例变量将会在对象实例化时随对象一起分配在java堆中。假设一个类变量定义为:
public static int value = 123;
变量value在准备阶段之后的初始值仍然为0,而不是123,此时尚未执行任何Java方法,把value赋值为123的putstatic
指令是程序编译后,存放于类构造器<clinit>()方法之中,要到类的初始化阶段才会执行。
如果类字段的字段属性表中存在ConstantValue属性,那么在准备阶段变量值就会被初始化为ConstantValue所指定的初始值:
public static final int value = 123;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。
4.解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
《Java虚拟机规范》未规定解析阶段发生的具体时间,只要求在执行anewarray、checkcast、getfield、隔天static、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc、ldc_w、ldc2_w、multianewarray、new、putfield、putstatic这17个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。
5.初始化
初始化阶段就是执行类构造器<clinit>()方法的过程。
- <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的
- <clinit>()方法与类的构造函数(即在虚拟机视角中的实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕
- 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作、
<clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法 - 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成 <clinit >()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法, 因为只有
- 父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也 一样不会执行接口的<clinit>()方法
- Java虚拟机必须保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的<clinit>()方法
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了