JVM类加载过程与加载器
Java类加载过程
Java类的生命周期
Java类加载过程是Java虚拟机(JVM)将类的字节码文件()加载到内存并进行初始化的过程。
JVM 可以理解的代码就叫做
字节码
(即扩展名为.class
的文件)
类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:
- 加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。
Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?
主要分为加载 、连接(验证、准备、解析)、初始化五个步骤。以下依次解读这五个步骤。
类加载过程
加载
在加载阶段,类加载器将类的字节码文件从文件系统、网络等位置读取到内存中。加载并不表示类的实例化,只是将类的信息加载到内存中。需要注意的是,这个阶段不会初始化类的静态变量和执行静态代码块。
加载这一步主要是通过类加载器 完成的。类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载由 双亲委派模型 决定(不过,我们也能打破由双亲委派模型)。
加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。
验证
在验证阶段,虚拟机对加载的字节码进行验证,确保其符合JVM规范,没有安全漏洞和不良代码。
这个过程是为了防止恶意代码的注入,以及确保类文件的正确性。但是如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用 -Xverify:none
参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
验证阶段主要由四个检验阶段组成:
文件格式验证(Class 文件格式检查)。基于该类的二进制字节流进行的,主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java 类型信息的要求。
元数据验证(字节码语义检查)。确保类文件中的信息与其声明一致,以及符号引用是否正确。虚拟机会验证类、父类、接口是否存在,字段和方法的名称和描述符是否正确,符号引用是否指向有效的实体等。
字节码验证(程序语义检查)。验证类的字节码是否合法、不会造成类型错误或其它异常。虚拟机会检查代码的类型转换、跳转目标是否合法,以及是否存在不合法的操作码序列等。
符号引用验证(类的正确性检查)。验证符号引用是否能够被正确解析。这个步骤确保被引用的类、字段、方法等在类加载过程中能够正确找到,并且能够被正确访问。
符号引用验证的主要目的是确保解析阶段能正常执行,如果无法通过符号引用验证,JVM 会抛出异常,比如:
java.lang.IllegalAccessError
:当类试图访问或修改它没有权限访问的字段,或调用它没有权限访问的方法时,抛出该异常。java.lang.NoSuchFieldError
:当类试图访问或修改一个指定的对象字段,而该对象不再包含该字段时,抛出该异常。java.lang.NoSuchMethodError
:当类试图访问一个指定的方法,而该方法不存在时,抛出该异常。
准备
在准备阶段,JVM为类的静态变量分配内存空间(这些内存都将在方法区中分配),并设置默认初始值。这里的初始值通常是数据类型的零值,如0、false、null等。
注意:
- 这里只是分配内存并设置初始值,并不包括对静态变量的赋值操作。初始化阶段才会赋值。
- 这时候进行内存分配的仅包括类变量( Class Variables ,即静态变量,被
static
关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
解析
解析阶段是将符号引用转换为直接引用的过程。
符号引用是编译时生成的、包含类、接口、字段或方法的全限定名的引用。直接引用是指向内存中实际数据的指针。解析有助于确定符号引用所对应的具体内存位置,以便在运行时能够正确访问。
初始化
初始化阶段是类加载的最后一个阶段,也是最关键的一步。在这个阶段,JVM会执行类的构造器<clinit>()
方法的代码,这个方法负责对静态变量进行初始化和执行静态代码块。
注意:
<clinit> ()
方法是编译之后自动生成的。- 父类的初始化在子类初始化之前发生
- 接口并不会在此阶段初始化。
类卸载
卸载类即该类的 Class 对象被 GC。
卸载类需要满足 3 个要求:
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被 GC
Java的类卸载通常是由JVM自动管理的,并不是开发者直接控制的。JVM的类加载器通常采用双亲委派模型,其中的类加载器有一个层次结构。当一个类加载器加载了一个类,它会一直在其类加载器的作用域内,直到不再需要。当类不再被需要时,类加载器及其加载的类可能会被卸载。
在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
类加载器
简介
类加载器ClassLoader
的主要作用就是加载 Java 类的字节码( .class
文件)到 JVM 中(在内存中生成一个代表该类的 Class
对象)
- 类加载器
ClassLoader
是一个负责加载类的对象,用于实现类加载过程中的加载这一步。 - 每个 Java 类都有一个引用指向加载它的
ClassLoader
。 - 数组类不是通过
ClassLoader
创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。 - 类加载器也可以加载文本、图像等资源,这里只讨论其核心功能 : 加载类 。
Java类加载器的分类 :
在Java中,类加载器分为以下几种类型:
- 启动类加载器(Bootstrap Class Loader):最顶层的加载类。是JVM的内置类加载器,负责加载JVM运行时需要的核心类,如
java.lang.Object
和java.lang.String
等。这个加载器通常由JVM实现提供,不是Java代码实现的。 - 扩展类加载器(Extension Class Loader):这个类加载器负责加载JRE扩展目录(
jre/lib/ext
)下的JAR文件中的类。这些类可以扩展JVM的功能。 - 应用程序类加载器(Application Class Loader):它负责加载应用程序的类,即开发者自己编写的类,负责加载当前应用 classpath 下的所有 jar 包和类。
- 自定义类加载器(Custom Class Loader):开发者可以通过继承
java.lang.ClassLoader
类来创建自己的类加载器。自定义类加载器可以用于实现各种加载策略,例如从网络、数据库或其他非传统位置加载类文件。
双亲委派模型
JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。
对于已经加载的类会被放在 ClassLoader
中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。
类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载呢?这就需要提到双亲委派模型了。
双亲委派的具体内容就是:
一个类加载器在加载某一个类的时候,首先委派给他的父类加载器去加载,依次递归,所以所有的加载请求其实都会汇聚到启动类加载器里面,如果父类加载器可以完成这个类的加载,那么就成功返回,如果加载不了,再由子类加载器进行加载。
1 | public Class<?> loadClass(String name) throws ClassNotFoundException { |
双亲委派的意义:
1、保证了每个类只加载一次,不会被重复加载;(双亲委派模型中的类加载器在加载类时会首先向上委派给父加载器,并且在每一级委派之前都会检查是否已加载,这就确保了类只会被加载一次。如果某个类加载器已经加载了一个类,那么它的父加载器会跳过加载步骤,直接返回已加载的类,从而避免了类的重复加载。)
2、保证了一些核心类库的安全性,一些规定好的只能是启动类加载器加载的那些。(由于核心类库是由启动类加载器加载的,应用程序的类加载器无法修改或篡改这些类。这有助于维护Java的核心类库的一致性和安全性。比如如果自定义了一个Integer类,但是由于双亲委派,最终还是会加载使用bootstrapClassLoader(启动类加载器)
加载的那个类。)
打破双亲委派模型
在双亲委派模型中,类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派父加载器完成(调用父类的 loadClass()
方法)
因此,要打破双亲委派机制,则重写 loadClass()
方法即可。
在loadClass
方法中,可以实现自己的类加载逻辑,而不是按照双亲委派模型的方式委派给父类加载器。这将允许您在应用程序中加载自定义的类,并绕过父类加载器。