Java 入门指南:JVM(Java虚拟机)—— Java 类加载器详解
类加载器(Class Loader)是 Java 虚拟机(JVM)的一部分,它的作用是将类的字节码文件(.class 文件)从磁盘或其他来源加载到 JVM 中。类加载器负责查找和加载类的字节码文件,并将其转化为 Class 对象。
类加载器
类加载器(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)中的工作过程是一个复杂而精细的流程。类加载器不仅负责加载类的字节码文件,还要确保类的正确性和初始化。
类加载器的工作过程可以分为以下几个主要阶段:
- 加载(Loading):在加载阶段,类加载器负责读取类的二进制数据,并将其转化为
Class
对象。这一阶段包括以下几个步骤:
- 查找或获取类的二进制数据:类加载器会根据类的全限定名(例如
com.example.MyClass
)查找并加载类的字节码文件。 - 生成
Class
对象:类加载器将字节码文件转化为Class
对象,并存放在方法区中。
-
验证(Verification):验证阶段是为了确保类文件的字节码符合 Java 虚拟机的规范,防止恶意代码危害虚拟机。验证阶段主要包括以下几个子阶段:
- 文件格式验证:确保字节流的格式符合 Class 文件格式规范。
- 元数据验证:确保类的元数据信息(如常量池中的常量)正确无误。
- 字节码验证:确保字节码指令符合 JVM 规范,不会导致非法操作。
- 符号引用验证:确保符号引用能正确解析到实际存在的类、接口、方法或字段。
-
准备(Preparation):准备阶段主要是为类变量分配内存空间,并设置类变量的初始值。注意,这里的类变量指的是被 static 修饰的变量。实例变量则是在对象实例化时分配内存空间。
-
解析(Resolution):解析阶段是将符号引用转换为直接引用的过程。符号引用指的是类名、接口名、方法名等字符串形式的引用,而直接引用则指向目标对象在内存中的地址。
-
初始化(Initialization):初始化阶段是执行类构造器 (
<clinit>
) 方法的过程。在这个阶段,类中的静态变量会被赋予初始值,并执行静态块中的代码。初始化阶段还包括类中非静态方法的调用和类的实例化。
更多推荐
所有评论(0)