Java 入门指南:JVM(Java虚拟机)—— Java 类加载器详解
类加载器
类加载器(Class Loader)是 Java 虚拟机(JVM)的一部分,它的作用是将类的字节码文件(.class
文件)从磁盘或其他来源加载到 JVM 中。类加载器负责查找和加载类的字节码文件,并将其转化为 Class 对象。
类加载器从 JDK 1.0 就出现了,最初只是为了满足 Java Applet
(已经被淘汰) 的需要。后来,慢慢成为 Java 程序中的一个重要组成部分,赋予了 Java 类可以被动态加载到 JVM 中并执行的能力。
根据官方 API 文档的介绍:
类加载器是一个负责加载类的对象。
ClassLoader
是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。
每个 Java 类都有一个引用指向加载它的
ClassLoader
。但数组类不是通过ClassLoader
创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()
方法获取ClassLoader
的时候和该数组的元素类型的ClassLoader
是一致的。
-
类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
-
每个 Java 类都有一个引用指向加载它的
ClassLoader
。 -
数组类不是通过
ClassLoader
创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。
组成部分
在 Java 中,类加载器主要有三个层次:
-
启动类加载器(Bootstrap ClassLoader):这是最基础的类加载器,由 C++ 实现,通常表示为 null,并且没有父级,负责加载扩展目录下的 jar 包和系统类路径下的核心库(
%JAVA_HOME%/lib
目录下的rt.jar
、resources.jar
、charsets.jar
等 jar 包和类)以及被-Xbootclasspath
参数指定的路径下的所有类。rt.jar
:rt 代表“RunTime”,rt.jar
是 Java 基础类库,包含 Java doc 里面看到的所有的类的类文件。也就是说,我们常用内置库java.xxx.*
都在里面,比如java.util.*
、java.io.*
、java.nio.*
、java.lang.*
、java.sql.*
、java.math.*
。 -
扩展类加载器(Extension ClassLoader):由 Java 实现,负责加载 Java 默认扩展目录下的 jar 包(
%JRE_HOME%/lib/ext
目录下的 jar 包和类以及被java.ext.dirs
系统变量所指定的路径下的所有类)。 -
系统类加载器(System/App ClassLoader):也称为应用程序类加载器,由 Java 实现,负责加载用户类路径(
classpath
)下的所有 jar 包和类。
除了这三个内置的类加载器外,还可以自定义类加载器,通过继承 java.lang.ClassLoader
类的方式实现,以满足特殊的需求。例如,可以通过自定义类加载器来加载网络上的类,或者从数据库中加载类。
对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class 对象不 equals
)。
ClassLoader
除了 BootstrapClassLoader
是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader
抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。
每个 ClassLoader
可以通过 getParent()
获取其父 ClassLoader
,如果获取到 ClassLoader
为 null
的话,那么该类是通过 BootstrapClassLoader
加载的。由于 BootstrapClassLoader
由 C++ 实现,由于这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null。
public abstract class ClassLoader {
...
// 父加载器
private final ClassLoader parent;
@CallerSensitive
public final ClassLoader getParent() {
//...
}
...
}
下面是一个获取 ClassLoader
的示例:
public class PrintClassLoaderTree {
public static void main(String[] args) {
ClassLoader classLoader = PrintClassLoaderTree.class.getClassLoader();
StringBuilder split = new StringBuilder("|--");
boolean needContinue = true;
while (needContinue){
System.out.println(split.toString() + classLoader);
if(classLoader == null){
needContinue = false;
}else{
classLoader = classLoader.getParent();
split.insert(0, "\t");
}
}
}
}
输出结果:
|--sun.misc.Launcher$AppClassLoader@18b4aac2
|--sun.misc.Launcher$ExtClassLoader@53bd815b
|--null
可以看出:
- 自定义编写的 Java 类
PrintClassLoaderTree
的ClassLoader
是AppClassLoader
; AppClassLoader
的父ClassLoader
是ExtClassLoader
;ExtClassLoader
的父ClassLoader
是Bootstrap ClassLoader
,因此输出结果为 null。
自定义类加载器
除了 BootstrapClassLoader
其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader
抽象类。
ClassLoader
类有两个关键的方法:
-
protected Class loadClass(String name, boolean resolve)
:加载指定二进制名称的类,实现了双亲委派机制。name
为类的二进制名称,resolve
如果为 true,在加载时调用resolveClass(Class<?> c)
方法解析该类。 -
protected Class findClass(String name)
:根据类的二进制名称来查找类,默认实现是空方法。
官方 API 文档中写到:
建议
ClassLoader
的子类重写findClass(String name)
方法而不是loadClass(String name, boolean resolve)
方法。
如果我们不想打破双亲委派模型,就需要重写 ClassLoader
类中的 findClass()
方法,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass()
方法。
实现自定义类加载器
以下是我们自行实现自定义类加载器的一个示例:
import java.io.*;
public class CustomClassLoader extends ClassLoader {
private String pathToBin;
public CustomClassLoader(String pathToBin) {
this.pathToBin = pathToBin;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] classData = loadClassData(name);
return defineClass(name, classData, 0, classData.length);
} catch (IOException e) {
throw new ClassNotFoundException("Class " + name + " not found", e);
}
}
private byte[] loadClassData(String name) throws IOException {
String file = pathToBin + name.replace('.', File.separatorChar) + ".class";
InputStream is = new FileInputStream(file);
ByteArrayOutputStream byteSt = new ByteArrayOutputStream();
int len = 0;
while ((len = is.read()) != -1) {
byteSt.write(len);
}
return byteSt.toByteArray();
}
}
示例说明:
- 构造器:接受一个字符串参数,这个字符串指定了类文件的存放路径。
- 覆写
findClass
方法:当父类加载器无法加载类时,findClass
方法会被调用。在这个方法中,首先使用loadClassData
方法读取类文件的字节码,然后调用defineClass
方法来将这些字节码转换为Class
对象。 loadClassData
方法:读取指定路径下的类文件内容,并将内容作为字节数组返回。
类加载器加载规则
JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。
对于已经加载的类会被放在 ClassLoader
中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。
public abstract class ClassLoader {
...
private final ClassLoader parent;
// 由这个类加载器加载的类。
private final Vector<Class<?>> classes = new Vector<>();
// 由 JVM 调用,用此类加载器记录每个已加载类。
void addClass(Class<?> c) {
classes.addElement(c);
}
...
}
类加载器工作过程
类加载器(Class Loader)在 Java 虚拟机(JVM)中的工作过程是一个复杂而精细的流程。类加载器不仅负责加载类的字节码文件,还要确保类的正确性和初始化。
JVM(Java虚拟机)——类的生命周期与加载过程
类加载器的工作过程可以分为以下几个主要阶段:
- 加载(Loading):在加载阶段,类加载器负责读取类的二进制数据,并将其转化为
Class
对象。这一阶段包括以下几个步骤:
- 查找或获取类的二进制数据:类加载器会根据类的全限定名(例如
com.example.MyClass
)查找并加载类的字节码文件。 - 生成
Class
对象:类加载器将字节码文件转化为Class
对象,并存放在方法区中。
-
验证(Verification):验证阶段是为了确保类文件的字节码符合 Java 虚拟机的规范,防止恶意代码危害虚拟机。验证阶段主要包括以下几个子阶段:
- 文件格式验证:确保字节流的格式符合 Class 文件格式规范。
- 元数据验证:确保类的元数据信息(如常量池中的常量)正确无误。
- 字节码验证:确保字节码指令符合 JVM 规范,不会导致非法操作。
- 符号引用验证:确保符号引用能正确解析到实际存在的类、接口、方法或字段。
-
准备(Preparation):准备阶段主要是为类变量分配内存空间,并设置类变量的初始值。注意,这里的类变量指的是被 static 修饰的变量。实例变量则是在对象实例化时分配内存空间。
-
解析(Resolution):解析阶段是将符号引用转换为直接引用的过程。符号引用指的是类名、接口名、方法名等字符串形式的引用,而直接引用则指向目标对象在内存中的地址。
-
初始化(Initialization):初始化阶段是执行类构造器 (
<clinit>
) 方法的过程。在这个阶段,类中的静态变量会被赋予初始值,并执行静态块中的代码。初始化阶段还包括类中非静态方法的调用和类的实例化。