一、类加载过程
1. 加载(Loading)
工作内容:
- 通过类的全限定名来获取定义此类的二进制字节流:
- JVM首先会调用类加载器的
findClass
方法来找到类文件的路径,通常从文件系统、JAR包、网络、数据库等来源获取类文件。
- JVM首先会调用类加载器的
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构:
- 将字节流解析为JVM内部的Class对象,包括类的基本信息,如类名、父类名、接口、字段、方法等。
- 在内存中生成一个代表这个类的Class对象:
- 在堆内存中生成一个Class对象,用来封装方法区中的类信息,并且将其与类的二进制字节流关联起来。
作用:
- 将类的字节码从外部存储加载到JVM内存中,并为其创建对应的Class对象,作为后续处理的基础。
2. 验证(Verification)
工作内容:
- 文件格式验证:
- 检查字节流的格式是否符合Class文件规范,例如是否以魔数0xCAFEBABE开头、主次版本号是否在支持的范围内等。
- 元数据验证:
- 检查类的元数据信息是否合法,如类的版本号、父类是否存在、类名是否合法等。
- 字节码验证:
- 通过数据流和控制流分析,确保字节码指令不会造成类型错误、栈溢出、下溢等问题。例如,确保方法的局部变量表和操作数栈在任何时候都具有正确的类型和大小。
- 符号引用验证:
- 确保类中的符号引用(例如对其他类、方法、字段的引用)是合法的,能够在运行时解析。例如,确保引用的类、字段、方法确实存在,方法的参数和返回类型正确等。
作用:
- 确保被加载的类是合法和安全的,防止恶意代码通过非法的字节码破坏JVM的稳定性,保护运行时环境。
3. 准备(Preparation)
工作内容:
- 为类的静态变量分配内存:
- 在方法区中为类的所有静态变量分配内存(不包括用
final
修饰的常量,因为常量在编译期已经被分配在常量池中)。
- 在方法区中为类的所有静态变量分配内存(不包括用
- 将静态变量初始化为默认值:
- 根据Java语言规范,基本数据类型(如int、float等)的静态变量初始化为其默认值(如0、0.0等),引用类型变量初始化为null。例如,
public static int a;
初始化为0,public static String s;
初始化为null。
- 根据Java语言规范,基本数据类型(如int、float等)的静态变量初始化为其默认值(如0、0.0等),引用类型变量初始化为null。例如,
作用:
- 为类的静态变量分配和初始化内存,确保类在初始化之前有正确的初始状态。
4. 解析(Resolution)
工作内容:
- 将常量池中的符号引用转换为直接引用:
- 符号引用是一组符号来描述目标(如类、字段、方法)的符号名称,直接引用是指向目标的实际内存地址或运行时数据结构。例如,将方法调用的符号引用转换为指向具体方法实现的直接引用。
- 具体解析过程:
- 类或接口的解析:将符号引用转换为具体的类或接口。
- 字段解析:将符号引用转换为字段的内存地址。
- 类方法和接口方法解析:将符号引用转换为方法的直接调用地址。
作用:
- 将符号引用解析为可以直接使用的内存地址或运行时数据结构,提高运行时的访问速度和效率。
5. 初始化(Initialization)
工作内容:
- 执行类构造器方法:
-
JVM自动收集类中的所有静态变量的赋值动作和静态代码块,合并生成类的构造器
<clinit>
方法(编译器自动收集,不需手工编写)。<clinit>
方法负责初始化静态变量
和执行静态代码块
。如果没有,就不会生成。 -
例如:
public class Test { static int x = 10; static { x = 20; } }
在初始化阶段会执行静态变量
x
的赋值和静态代码块,将x
初始化为20public class Test { static { x = 20; } static int x = 10; }
指令语句按照源文件中出现的顺序执行。此处不会报错,将
x
初始化为20,然后在初始化为10;
-
- 确保父类的初始化:
- 在初始化一个类之前,首先需要确保其父类已经初始化。例如,在初始化子类之前,JVM会递归地先初始化其所有父类。
- 初始化静态变量:
- 根据程序中的静态变量赋值语句进行赋值。
- 执行静态代码块:
- 执行类中的静态代码块,进行必要的初始化操作。
作用:
- 初始化类的静态变量和静态代码块,确保类在使用之前已经完成必要的初始化工作,保证程序的正确性和一致性。
通过这五个阶段,JVM确保类从加载到使用的全过程中,每一步都经过严格的检查和处理,从而保证了Java程序的安全性、稳定性和正确性。
二、 类与类加载器
通过一个类的全限定名来获取描述该类的二进制字节流
1.类加载器(Class Loader)
定义:
- 类加载器是负责在运行时加载类的Java组件。它从不同的源(如文件系统、网络等)加载类的字节码,并将其转换为JVM可以执行的类对象。
工作原理:
- 类加载器遵循双亲委派模型。每个类加载器都有一个父类加载器,当需要加载一个类时,类加载器会首先委托父类加载器进行加载,如果父类加载器无法找到该类,它才会尝试自己加载。
2.类加载器的分类
狭义上讲,JVM支持两种类型的加载器:引导类加载器和自定义类加载器。
自定义类加载器指的是:派生于抽象类ClassLoader的类加载器。
JVM中有多种类加载器,根据加载的优先级和作用范围,可以分为以下几类:
-
引导类加载器(Bootstrap Class Loader):
- 位置:JVM内部实现,通常由C++语言实现。
- 加载范围:负责加载JDK核心类库(如
java.lang.*
、java.util.*
等)和核心JAR包。还要加载扩展类加载器和程序类加载器
- 特点:是JVM启动时第一个被加载的类加载器,没有父类加载器。
-
扩展类加载器(Extension Class Loader):
- 位置:由Java实现,通常是
sun.misc.Launcher$ExtClassLoader
的实例。派生于ClassLoader类 - 加载范围:加载扩展目录
lib/ext
下的类库(JAR文件)。 - 父类加载器:引导类加载器。
- 位置:由Java实现,通常是
-
应用程序类加载器(Application/System Class Loader):
- 位置:由Java实现,通常是
sun.misc.Launcher$AppClassLoader
的实例。派生于ClassLoader类 - 加载范围:加载用户类路径(classpath)上的类,我们开发的类都是由它来完成加载的。
- 父类加载器:扩展类加载器。
- 位置:由Java实现,通常是
-
自定义类加载器(Custom Class Loader):
- 位置:用户可以自定义实现
java.lang.ClassLoader
类。 - 加载范围:根据用户需求自定义加载范围,可以从网络、数据库等非标准路径加载类。
- 父类加载器:可以是应用程序类加载器或其他类加载器。
- 位置:用户可以自定义实现
3.类加载器的双亲委派模型
工作流程:
- 委派机制:当一个类加载器接到加载类的请求时,它不会自己尝试加载这个类,而是将请求委派给父类加载器。
- 自顶向下:如果父类加载器找不到目标类,才会由当前类加载器自己进行加载。
优点:
- 安全性:确保核心类库不会被用户自定义的类库替代,避免了核心类库被篡改的风险。
- 一致性:确保同一个类在不同的类加载器环境中只有一个版本,避免了类的重复加载和版本冲突。
注意
:不同类加载器加载同一个类
比较结果:
- 如果不同的类加载器加载同一个类(即使类名和包名完全相同),它们在JVM中也会被视为不同的类。
- 原因是类的唯一性不仅由类名决定,还由加载它的类加载器决定。
示例:
ClassLoader classLoader1 = new CustomClassLoader();
ClassLoader classLoader2 = new CustomClassLoader();
Class<?> class1 = classLoader1.loadClass("com.example.MyClass");
Class<?> class2 = classLoader2.loadClass("com.example.MyClass");
boolean areClassesEqual = class1.equals(class2); // 结果为false
在上述示例中,虽然class1
和class2
的全限定名相同,但是由于它们是由不同的类加载器加载的,因此它们被认为是不同的类,areClassesEqual
结果为false
。
三、双亲委派模型
定义:
JVM的双亲委派机制(Parent Delegation Model)是一种类加载机制,它确保类加载请求按照层次结构从子类加载器向父类加载器递归传递,直到找到合适的类加载器进行加载。如果父类加载器无法加载该类,子类加载器才会尝试加载。
1.工作原理
工作流程:
- 类加载请求:当一个类加载器接到一个类加载请求时,它首先不会直接尝试加载该类。
- 委派父类加载器:它会将这个类加载请求委派给父类加载器(如果存在)。
- 递归委派:这种委派是递归的,每个父类加载器又会将请求向上委派,直至顶层的引导类加载器(Bootstrap Class Loader)。
- 类加载:如果引导类加载器能够加载该类,则加载过程结束。如果引导类加载器无法加载该类,则请求会依次返回到下层的类加载器,最终由最初的类加载器尝试加载该类。
具体过程:
- 启动类加载器(Bootstrap ClassLoader):它是整个加载体系的顶层,由JVM实现,用于加载核心类库(如rt.jar)。
- 扩展类加载器(Extension ClassLoader):它从JVM的扩展目录(如lib/ext目录)加载类。
- 应用程序类加载器(Application ClassLoader):它从classpath指定的路径加载应用程序的类。
- 自定义类加载器(Custom ClassLoader):用户可以根据需要创建自己的类加载器,通常用于加载非标准路径的类。
2.意义和作用
1. 安全性:
- 防止核心类库被篡改:核心类库(如
java.lang.*
、java.util.*
等)由引导类加载器加载,不会被用户自定义类加载器篡改。例如,用户不能通过自定义类加载器替换系统核心类,这样可以防止恶意代码攻击和破坏JVM的安全性。
2. 避免类重复加载:
- 类加载的一致性:同一个类在不同的加载器环境中应该只有一个版本。双亲委派机制通过向上委派确保类只会被加载一次,避免重复加载导致的类不一致问题。
- 内存效率:减少类的重复加载,可以节省内存,提高内存利用率。
3. 模块化设计:
- 模块解耦:双亲委派机制允许不同模块(如核心类库、扩展类库、应用类库)使用不同的类加载器,模块之间通过委派机制进行类加载请求的传递,使得模块之间更加独立和解耦。
3.实际应用中的场景
1. 应用服务器和框架:
- 例如Tomcat、Spring等:它们使用自定义类加载器加载应用程序的类和库,而核心类库和框架类库则由应用类加载器或其父类加载器加载。这种设计保证了框架和应用程序的独立性和安全性。
2. 插件系统:
- 例如Eclipse、IDEA等:插件系统通常需要动态加载和卸载插件。通过自定义类加载器,插件可以在隔离的类加载器环境中运行,不会影响其他插件或主程序。
通过上述机制和设计,双亲委派机制确保了Java类加载过程的安全性、一致性和模块化,使得Java应用程序能够在复杂的类加载环境中稳定运行。
四、其他
1、JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载
的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
2、Java程序对类的使用方式分为:主动使用和被动使用
主动使用,又分为七种情况:
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(比如:Class.forName(“com.atguigu.Test"))初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类
- JDK 7开始提供的动态语言支持:
- java.lang.invoke.MethodHandle实例的解析结果REF getstatic、REF putstatic、REF invokestatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用都不会导致类的初始化。