那么让我们来详细讲解一下“一篇文章弄懂JVM类加载机制过程以及原理”的完整攻略。
1. JVM类加载机制基础
在深入了解JVM类加载机制的过程之前,我们需要先了解一些相关的基础知识。
1.1 类、类加载器和类加载
在Java中,我们通常所说的类是指Java类,而Java类的定义是以.java
文件为载体,通过编译器(如javac)将其转换为.class
文件后生成,.class
文件就是Java类的二进制字节码形式。Java虚拟机通过字节码来执行Java程序,因此Java虚拟机加载的是.class
文件。
类加载器是Java虚拟机的组成部分之一,用于把类加载到内存中并生成Java类对象。一个Java类在被加载到内存中之前,需要经过三个步骤:加载、链接和初始化。其中加载即是将Java类的字节码文件读取到内存中,并创建一个对应的java.lang.Class
对象的过程。
1.2 类的生命周期
一个Java类从被加载到虚拟机内存中开始,到虚拟机将其卸载出内存为止,其整个生命周期可以划分为如下7个阶段:
- 加载:将类的
.class
文件读入内存,并在堆空间创建一个用于描述类的java.lang.Class
对象。 - 验证:验证类的字节码文件的正确性,包括文件格式、语义、二进制兼容性等。
- 准备:为类的静态变量分配内存,并设置默认值。
- 解析:将常量池中的符号引用转换为直接引用,即将常量池中的类名、方法名、字段名等转换为内存地址。
- 初始化:为类的静态变量赋值,并执行静态代码块。
- 使用:类从虚拟机中加载后,才能被Java程序调用使用。
- 卸载:类在虚拟机内不再使用,卸载类并释放内存。
1.3 类加载器分类
Java虚拟机的类加载器有三种,按照类加载器的启动顺序可分为以下三类:
- 启动类加载器(Bootstrap ClassLoader):用于加载Java虚拟机核心库中的类,如
rt.jar
、resources.jar
等。 - 扩展类加载器(Extension ClassLoader):用来加载Java虚拟机扩展目录中的类库,即
$JAVA_HOME/jre/lib/ext
目录下的所有jar
包。 - 应用程序类加载器(Application ClassLoader):又称为系统类加载器,用于加载当前应用程序类路径(即
CLASSPATH
)下的全部类库。
1.4 双亲委派模型
Java虚拟机的类加载器采用了一种父亲委派的加载机制,即双亲委派模型。当一个类被加载到虚拟机中时,会先从启动类加载器开始依次向下搜索,直到找到该类为止。如果找不到,则会抛出ClassNotFoundException
异常。
具体来说,类加载器在加载一个类时,首先会尝试使用其父亲类加载器去加载该类,只有在父亲类加载器无法加载该类时才会尝试自己加载该类。这样做的好处是防止重复加载,提高安全性和系统稳定性。
在实际应用中,一般自定义类加载器都是基于父类加载器进行构建的,而双亲委派模型也是Java虚拟机对于Java类的安全保障机制之一。
2. JVM类加载机制过程
了解了上述的基础知识之后,我们可以开始深入了解JVM类加载机制的过程。
2.1 类加载的过程
类加载的过程是在“使用”阶段之前,即“初始化”阶段。在“初始化”阶段,Java虚拟机才会真正执行类的初始化,也才算是对该类的主动使用。
类加载的过程主要分为以下三个阶段:
- 加载:从文件系统或网络中读入字节码流,将其转换成
java.lang.Class
对象。 - 链接:将Java类的
.class
文件中的符号引用(如类和接口的全限定名、字段和方法的名称等)解析为直接引用(如在运行时内存中的对象、方法区中的指针等),并把刚刚加载的.class
文件中的变量初始化为默认值(如0或null)。 - 初始化:执行类的初始化代码,包括静态初始化代码块和变量初始化等。当一个类被初始化时,依次执行此类父类的静态代码块和构造方法。
2.2 类加载器的实现过程
Java虚拟机在加载Java类时,会使用Java类加载机制来完成下面的三个过程:
- 将Java类的字节码文件加载到Java虚拟机内存中:每个Java类都有一个与之对应的
java.lang.Class
对象,该对象记录了这个类的完整信息。 - 验证Java类的字节码文件的正确性:验证阶段需要检查字节码文件的文件格式(Magic Number)、语义合法性和二进制兼容性等。
- 让内存中的Java类“可用”:包括初始化类的数据和代码,初始化阶段主要是执行类的静态初始化代码块和静态变量初始化。为了实现这些操作,Java虚拟机需要使用到类加载器。
类加载器在加载Java类的过程中,通常会遵循如下流程:
- 检查类是否已经被加载过,如果已经被加载过,则直接返回。
- 如果类没有被加载过,则让其父加载器(除了启动类加载器)试图加载该类。
- 如果父加载器无法加载该类,则调用自己的加载方法进行加载。
一个类的加载过程可以简单地概括为:类加载器在加载某个类时,首先会代理向其父类加载器发送加载请求,以此类推,直到请求传递到最顶层,即启动类加载器。如果最顶层的类加载器可以找到该类,则将该类加载到内存中;如果所有的类加载器都无法找到该类,则会抛出ClassNotFoundException
异常。
2.3 类的双亲委派机制
Java虚拟机的类加载器采用了双亲委派机制。当Java虚拟机在加载某个类时,会依次向其父类加载器发送加载请求,直到顶级类加载器——启动类加载器。如果启动类加载器也无法找到该类,则会抛出ClassNotFoundException
异常。
采用双亲委派机制的好处在于:
- 可以防止类的重复加载:当某个类在某个类加载器的加载范围内已经被加载,其他类加载器如果需要加载该类,只需要将该请求委托给其父类加载器进行处理即可。
- 可以保证Java核心类的安全:Java核心类库都是由启动类加载器加载,而启动类加载器是由Java虚拟机实现的,因此任何不受信任的代码都无法通过其父加载器加载核心类库,从而保证了Java核心类的安全性。
3. 示例说明
下面通过两个例子来帮助更好地理解JVM类加载机制的过程。
示例1
假设有如下Java代码:
public class Test {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
当我们编译运行该代码时,Java虚拟机需要先加载该类,然后才能开始执行。
在执行Java代码时,Java虚拟机首先需要将Java类的.class
文件读入内存,并在堆空间创建一个java.lang.Class
对象。在进行类加载的过程中,Java虚拟机采用了双亲委派机制。具体来说,Java虚拟机会按照以下的顺序进行类加载:
- 首先,Java虚拟机会由应用程序类加载器(也叫系统类加载器)尝试加载该类,但该类不在应用程序类加载器的加载范围内。
- 随后,应用程序类加载器会代理向扩展类加载器发送加载请求,扩展类加载器无法加载该类,因此继续向其父加载器——启动类加载器发送加载请求。
- 启动类加载器成功加载该类。
最终,Java虚拟机将该类加载到内存中,并创建一个对应的java.lang.Class
对象,该对象用于描述该类的完整信息。
示例2
假设我们需要自定义一个类加载器,并使用其去加载一个类。我们可以在自定义类加载器中重写findClass
方法,并将该类放在指定的Class路径下:
public class MyClassLoader extends ClassLoader {
/**
* 定义加载的目录
*/
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
/**
* 重写findClass方法
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 将类名转换为对应的文件路径
String fileName = name.replace(".", "/");
// 拼接完整的文件路径
String filePath = classPath + "/" + fileName + ".class";
// 读取字节码文件到内存中
byte[] classBytes = loadClassBytes(filePath);
if (classBytes == null) {
throw new ClassNotFoundException("Cannot load class " + name);
}
// 将字节码转换为Class对象
return defineClass(name, classBytes, 0, classBytes.length);
}
/**
* 读取字节码文件到内存中
*/
private byte[] loadClassBytes(String fileName) {
// 从文件中读取字节码文件
// ...
}
}
我们可以使用以下代码来测试该类加载器:
public static void main(String[] args) throws Exception {
// 创建自定义类加载器
MyClassLoader myClassLoader = new MyClassLoader("/path/to/class");
// 在自定义类加载器中载入某个类
Class<?> clazz = myClassLoader.loadClass("com.example.MyClass");
// 创建该类的实例
Object instance = clazz.newInstance();
// 调用该类的某个方法
Method method = clazz.getMethod("xxx", String.class);
method.invoke(instance, "Hello, World!");
}
在这个例子中,我们自定义了一个类加载器,并将其用于载入一个指定目录下的类。类加载器会先尝试使用父类加载器加载该类,如果父类加载器无法加载,则代理调用自己的类加载方法进行加载。
值得注意的是,在本例中,我们自定义了一个类加载器,并将字节码文件读入到内存中。在实际应用中,JVM类加载机制也是如此,Java虚拟机将Java类的字节码文件读取到内存中,并创建一个对应的java.lang.Class
对象。类加载器的作用在于将Java类加载到内存中,并生成对应的Class对象。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:一篇文章弄懂JVM类加载机制过程以及原理 - Python技术站