Java类加载机制
概述
JVM把描述类文件的数据从.class文件加载到内存,并对数据进行校验、转换、解析和类初始化,最初形成可以被JVM使用的Java类型,这个过程被称为JVM的类加载机制。
Java类加载过程机制允许在运行时加载类,而不再局限于在编译时确定依赖。这种特性赋予了Java动态性和灵活性,使得用于可以在无需重启的情况下动态的引入新功能。
类加载过程
类的生命周期,是从加载到JVM内存开始,到卸载出JVM内存结束
整个声明周期包括:加载、验证、准备、解析、初始化、使用、卸载
其中从验证到解析被称为连接,从加载到初始化称为类加载。
加载
该阶段主要作用是查找并加载类的二进制数据
加载阶段:
- 通过类的全限定名(包名+类名)来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为运行时的数据结构
- 在内存中生成一个代表这个类的
java.lang.Class对象,作为方法区这个类的各种数据的访问入口
连接
验证
用于确保被加载类的正确性
验证的内容:
文件格式验证
验证字节流是否符合Class文件格式规范
元数据验证
对字节码描述的信息进行语义分析,以确保其描述的信息符合Java语言规范要求
字节码验证
通过数据流和控制流分析,确保程序定义是否合法、符合逻辑
符号引用验证
确保解析动作能正确执行
可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短JVM类加载的时间。
准备
为类的静态变量分配内存,并将其初始化为默认值
此阶段会在JVM内存中的方法区进行:
内存分配仅包括类静态变量,实例变量将会在对象实例化时随着对象一起分配到Java堆中。
这里所设置的初始值通常情况下是数据类型默认的零值(如
0、0L、null、false等),引用类型为null1
2
3
4
5
6
7
8
9
10
11
12public class Main {
public static int i;
public static String s;
public static boolean b;
public static void main(String[] args) {
System.out.println(i); //0
System.out.println(s); //null
System.out.println(b); //false
}
}对于同时被static和final修饰的常量,必须在声明的时候就为其显示赋值;只被final修饰的常量,在使用前必须为其显示赋值,系统不会为其赋予默认零值。否则IDEA会提示未初始化
解析
把类中的符号引用转换为直接引用
符号引用就是一组符号来描述目标,可以是任何字面量。
例如,类
A调用类B的method(),编译后.class文件中会记录B的类名、方法名及描述符,而非实际地址。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
1 | // 类A调用类B的静态方法 |
初始化
执行类构造器<clinit>()方法的过程,用于初始化静态变量和静态代码块
会调用java.lang.ClassLoader加载字节码,ClassLoader会调用JVM的native方法(defineClass0/1/2)来定义一个java.lang.Class
其中包括:
- 执行static语句块中的语句
- 完成static属性的赋值操作
- 当类的直接父类还没有被初始化,则先初始化其父类,即父类中定义的静态语句块优先于子类的变量赋值操作
如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
类的加载时机
JVM会在程序第一次注定引用类的时候加载该类,被动引用时并不会引用类加载的操作
被动引用可能触发类的加载,但不会触发初始化
主动引用
遇到
new、getstatic、putstatic、invokestatic字节码指令new实例化对象new指令需要访问类的构造函数,并分配对象内存,必须确保类已初始化getstatic/putstatic读取设置类的静态属性(被final修饰,编译期把结果放入常量池中的静态字段除外)invokestatic调用类的静态方法
JVM启动,先初始化包含
main()方法的主类初始化一个类时,其父类还没初始化(需先初始化父类)
对类进行反射调用
JDK 1.7动态语言支持:一个
java.lang.invoke.MethodHandle的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic。
被动引用
- 通过子类引用父类的静态变量,不会导致子类初始化
- 定义类的数组类型
Array[] arr = new Array[10];不会触发Array类初始化 static final VAR在编译阶段会存入调用类的常量池,通过ClassName.VAR引用不会触发ClassName初始化- 通过类名获取Class对象
类加载器
Java把类加载阶段中的**”通过一个类的全限名来获取描述此类的二进制字节流”这个动作放到JVM外部实现**,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为”类加载器“。
分类
类加载器的分类:
启动类加载器
C++语言实现,不继承
java.lang.ClassLoader,不能被Java程序直接调用,是JVM自身的一部分。负责将<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径下的类库加载到JVM内存中,用于加载Java的核心库如java.lang.*、java.util.*等。该加载器在Java中无法获取其引用(
ClassLoader.getParent()返回null),是所有类加载器的祖先,没有父加载器1
2// 尝试获取String类的类加载器(返回null,表示由Bootstrap加载)
ClassLoader loader = String.class.getClassLoader(); // null扩展类加载器
负责加载
<JAVA_HOME>\lib\ext目录下的类库,或者被java.ext.dirs系统变量所指定的路径中的所有类库。用于加载Java扩展库,开发者可以直接使用这个类加载器(可通过ClassLoader.getSystemClassLoader().getParent()获得)1
2// 加载JDK扩展库中的类(如javax包下的类)
ClassLoader extLoader = ClassLoader.getSystemClassLoader().getParent();应用程序类加载器
这个类加载器负责加载用户路径(CLASSPATH)下的类库,一般我们编写的Java类都是由这个类加载器加载,这个类加载器是ClassLoader中的
getSystemClassLoader()方法的返回值,所以也称为系统类加载器。一般情况下这就是系统默认的类加载器。1
2// 获取系统类加载器(默认加载用户类路径的类)
ClassLoader appLoader = ClassLoader.getSystemClassLoader();自定义类加载器
通过继承
ClassLoader并重写方法,主要用于热部署、模块隔离、加密加载
类加载器的层级关系:
1 | Bootstrap ClassLoader(启动类加载器,C++实现) |
1 | public class ClassLoaderDemo { |
1 | 系统类加载器: sun.misc.Launcher$AppClassLoader@4e0e2f2a |
类加载器的核心方法
对应ClassLoader对象
loadClass(String name): 加载指定的Java类findClass(String name): 查找指定的Java类findLoadedClass(String name): 查找JVM已经加载过的类defineClass(String name, byte[] b, int off, int len): 定义一个Java类将字节数组转换为JVM内部的
Class对象,是类加载的核心步骤。注意此方法为final方法,不可重写resolveClass(Class<?> c): 链接指定的Java类,包括验证、准备、解析三个阶段
双亲委派模型
JVM并不是在启动时就把.class文件都加载了一遍,而是在执行过程中用到了这个类才去加载
如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器也是如此,因此所有的加载请求都应该被传送到顶层的启动类加载器中,当最顶层的启动类加载器无法加载该类时,再一层一层向下委派给子类加载器
1 | public abstract class ClassLoader { |
类加载的方式
- 命令行启动应用时候JVM初始化加载
- 通过
Class.forName方法动态加载 - 通过
ClassLoader.loadClass()方法动态加载
通过Class.forName方法动态执行类中的static静态代码块,而ClassLoader.loadClass()方法动态加载不会执行
类加载过程实践
1 | public class Main { |
这里我们创建了个java文件,当我们运行时,首先javac会将文件编译转换为JVM可识别的字节码文件,生成.class文件,内容为二进制字节码。然后JVM进行类加载机制:
可以看到,获取到的是APPClassLoader加载器
然后执行ClassLoader.loadClass方法。这里由于AppClassLoader类没有loadClass(string name)方法,于是调用父类的ClassLoader.loadClass方法。
然后调用loadClass(String var1, boolean var2)方法:
最后会调用父类的loadClass(String name, boolean resolve)方法:
在父类的loadClass(String name, boolean resolve)方法中:
这里判断父类加载器是否加载过这个类,如果没有会调用父类加载器的loadClass方法进行Java,反之自己加载
可以看到父类加载器为ExtClassLoader
最后执行到URLClassLoader.findClass⽅法:
随后调用SecureClassLoader.defineClass(name, b, off, len, getProtectionDomain(cs));
最后是调⽤了这个ClassLoader.defineClass1⽅法:
总结:
- 类的继承关系(左⼦类右⽗类):
AppClassLoader → URLClassLoader → SecureClassLoader → ClassLoader - 类加载时的⽅法调⽤:
loadClass → findClass → defineClass - findClass是判断该路径下能否加载该类,defineClass是通过字节码加载类
在调试过程遇到的一个类加载器URLClassLoader,它可以加载本地和远程的class文件:
1 | import java.net.URL; |
1 | public class Test { |
输出nivia,说明成功加载到本地的class文件
利用加载器获取Class对象
ClassLoader类下存在一个静态方法getSystemClassLoader(),可以获取到AppClassLoader应用程序加载器
1 | System.out.println(ClassLoader.getSystemClassLoader()); |
可利用获取到的AppClassLoader应用程序加载器,来加载一个类:
1 | ClassLoader.getSystemClassLoader().loadClass("java.lang.Runtime") |
参考
https://www.cnblogs.com/czwbig/p/11127222.html
https://www.cnblogs.com/happy-coding/p/18692970#%E7%AE%80%E4%BB%8B

.webp)















