概述

JVM把描述类文件的数据从.class文件加载到内存,并对数据进行校验、转换、解析和类初始化,最初形成可以被JVM使用的Java类型,这个过程被称为JVM的类加载机制。

Java类加载过程机制允许在运行时加载类,而不再局限于在编译时确定依赖。这种特性赋予了Java动态性和灵活性,使得用于可以在无需重启的情况下动态的引入新功能。

类加载过程

类的生命周期,是从加载到JVM内存开始,到卸载出JVM内存结束

整个声明周期包括:加载、验证、准备、解析、初始化、使用、卸载

其中从验证到解析被称为连接,从加载到初始化称为类加载。

加载

该阶段主要作用是查找并加载类的二进制数据

加载阶段:

  • 通过类的全限定名(包名+类名)来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为运行时的数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

连接

验证

用于确保被加载类的正确性

验证的内容:

  • 文件格式验证

    验证字节流是否符合Class文件格式规范

  • 元数据验证

    对字节码描述的信息进行语义分析,以确保其描述的信息符合Java语言规范要求

  • 字节码验证

    通过数据流和控制流分析,确保程序定义是否合法、符合逻辑

  • 符号引用验证

    确保解析动作能正确执行

可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短JVM类加载的时间。

准备

为类的静态变量分配内存,并将其初始化为默认值

此阶段会在JVM内存中的方法区进行:

  • 内存分配仅包括类静态变量实例变量将会在对象实例化时随着对象一起分配到Java堆中。

  • 这里所设置的初始值通常情况下是数据类型默认的零值(如00Lnullfalse等),引用类型为null

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public 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调用类Bmethod(),编译后.class文件中会记录B的类名、方法名及描述符,而非实际地址。

  • 直接引用就是直接指向目标的指针相对偏移量或一个间接定位到目标的句柄

1
2
3
4
5
6
7
8
9
10
// 类A调用类B的静态方法
public class A {
public static void main(String[] args) {
B.staticMethod(); // 符号引用解析为B.staticMethod()的直接地址
}
}

public class B {
public static void staticMethod() {}
}

初始化

执行类构造器<clinit>()方法的过程,用于初始化静态变量和静态代码块

会调用java.lang.ClassLoader加载字节码,ClassLoader会调用JVM的native方法(defineClass0/1/2)来定义一个java.lang.Class

其中包括:

  • 执行static语句块中的语句
  • 完成static属性的赋值操作
  • 当类的直接父类还没有被初始化,则先初始化其父类,即父类中定义的静态语句块优先于子类的变量赋值操作

如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

类的加载时机

JVM会在程序第一次注定引用类的时候加载该类,被动引用时并不会引用类加载的操作

被动引用可能触发类的加载,但不会触发初始化

主动引用

  • 遇到newgetstaticputstaticinvokestatic字节码指令

    • 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
2
3
4
5
6
7
Bootstrap ClassLoader(启动类加载器,C++实现)

Extension ClassLoader(扩展类加载器,Java实现)

Application ClassLoader(应用程序类加载器,Java实现)

Custom ClassLoader(自定义类加载器,Java实现)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ClassLoaderDemo {
public static void main(String[] args) {
// 获取系统类加载器(Application ClassLoader)
ClassLoader appLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器: " + appLoader);

// 获取其父加载器(Extension ClassLoader)
ClassLoader extLoader = appLoader.getParent();
System.out.println("扩展类加载器: " + extLoader);

// 尝试获取扩展类加载器的父加载器(Bootstrap ClassLoader,返回null)
ClassLoader bootstrapLoader = extLoader.getParent();
System.out.println("启动类加载器: " + bootstrapLoader);
}
}
1
2
3
系统类加载器: sun.misc.Launcher$AppClassLoader@4e0e2f2a
扩展类加载器: sun.misc.Launcher$ExtClassLoader@1540e19d
启动类加载器: null

类加载器的核心方法

对应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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public abstract class ClassLoader {

//每个类加载器都有个父加载器
private final ClassLoader parent;

public Class<?> loadClass(String name) {

//查找一下这个类是不是已经加载过了
Class<?> c = findLoadedClass(name);

//如果没有加载过
if( c == null ){
//先委派给父加载器去加载,注意这是个递归调用
if (parent != null) {
c = parent.loadClass(name);
}else {
// 如果父加载器为空,查找Bootstrap加载器是不是加载过了
c = findBootstrapClassOrNull(name);
}
}
// 如果父加载器没加载成功,调用自己的findClass去加载
if (c == null) {
c = findClass(name);
}

return c;
}

protected Class<?> findClass(String name){
//1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存
...

//2. 调用defineClass将字节数组转成Class对象
return defineClass(buf, off, len);
}

// 将字节码数组解析成一个Class对象,用native方法实现
protected final Class<?> defineClass(byte[] b, int off, int len){
...
}
}

类加载的方式

  • 命令行启动应用时候JVM初始化加载
  • 通过Class.forName方法动态加载
  • 通过ClassLoader.loadClass()方法动态加载

通过Class.forName方法动态执行类中的static静态代码块,而ClassLoader.loadClass()方法动态加载不会执行

类加载过程实践

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) throws Exception {
ClassLoader cl = ClassLoader.getSystemClassLoader();
cl.loadClass("nivia");
}
}
class nivia{
static {
System.out.println("static");
}
}

这里我们创建了个java文件,当我们运行时,首先javac会将文件编译转换为JVM可识别的字节码文件,生成.class文件,内容为二进制字节码。然后JVM进行类加载机制:

可以看到,获取到的是APPClassLoader加载器

然后执行ClassLoader.loadClass方法。这里由于AppClassLoader类没有loadClass(string name)方法,于是调用父类的ClassLoader.loadClass方法。

然后调用loadClass(String var1, boolean var2)方法:

最后会调用父类的loadClass(String name, boolean resolve)方法:

QQ20250402-213226

在父类的loadClass(String name, boolean resolve)方法中:

QQ20250402-202817

这里判断父类加载器是否加载过这个类,如果没有会调用父类加载器的loadClass方法进行Java,反之自己加载

可以看到父类加载器为ExtClassLoader

最后执行到URLClassLoader.findClass⽅法:

随后调用SecureClassLoader.defineClass(name, b, off, len, getProtectionDomain(cs));

最后是调⽤了这个ClassLoader.defineClass1⽅法:

总结:

  1. 类的继承关系(左⼦类右⽗类):AppClassLoader → URLClassLoader → SecureClassLoader → ClassLoader
  2. 类加载时的⽅法调⽤:loadClass → findClass → defineClass
  3. findClass是判断该路径下能否加载该类,defineClass是通过字节码加载类

在调试过程遇到的一个类加载器URLClassLoader,它可以加载本地和远程的class文件:

1
2
3
4
5
6
7
8
9
10
import java.net.URL;
import java.net.URLClassLoader;

public class Main {
public static void main(String[] args) throws Exception {
URLClassLoader urlclassloader = new URLClassLoader(new URL[]{new URL("file:///Users/nivia/Desktop/Java/src")});
Class c = urlclassloader.loadClass("Test");
c.newInstance();
}
}
1
2
3
4
5
public class Test {
{
System.out.println("nivia");
}
}

输出nivia,说明成功加载到本地的class文件

利用加载器获取Class对象

ClassLoader类下存在一个静态方法getSystemClassLoader(),可以获取到AppClassLoader应用程序加载器

1
2
System.out.println(ClassLoader.getSystemClassLoader()); 
//sun.misc.Launcher$AppClassLoader@18b4aac2

可利用获取到的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

https://javabetter.cn/jvm/class-load.html

https://nivi4.notion.site/Java-cedccc0611654bd99f841de3ef578e24?pvs=97#c713bd160a704c2ebc8e917484959e98