1. 类加载过程
1.1 类加载概述
类加载是Java虚拟机(JVM)将类的字节码加载到内存,并将其转换为可以被JVM执行的对象的过程。Java的类加载是Java语言的重要特性之一。
学习类加载有助于我们更深入地理解 JAVA 类成员的初始化过程和运行过程,可以为后续解决线上问题及调优提供一种基础保障。
1.2 类的生命周期
类的生命周期指的是从加载到卸载的基本过程,此过程包含 7 个阶段,如下图所示:
大的阶段可以分为类的加载、类的使用、以及类的卸载。
类的加载阶段分为加载、链接、初始化三个大的阶段,其中链接过程又包含了验证、准备和解析。
1.3 加载(Loading)
加载阶段的任务包括将字节码数据转换成方法区内部表示的运行时数据结构,并在Java堆中生成一个代表该类的Class对象。
类加载由类加载器完成,类加载器根据类的全限定名(类全名)定位并读取类的字节码文件。
1.4 加载方式及时机
隐式加载和显式加载是类加载过程中的两种加载方式。它们的区别在于触发类加载的时机和方式。
1、隐式加载(Implicit Loading)
- 隐式加载是指在程序执行过程中,当需要使用某个类时,由JVM自动触发类的加载过程
- 当程序代码中引用了一个类,并且该类尚未被加载到内存中时,JVM会自动触发该类的加载过程
- 隐式加载通常发生在类的实例化、访问静态成员、调用静态方法等操作时
- 构建子类实例对象时会先加载父类类型
- 隐式加载是Java中常见的类加载方式,大部分类的加载都是隐式加载
2、显式加载(Explicit Loading)
- 显式加载是指开发者在代码中显式调用类加载器的相关方法来加载指定的类
- 显式加载可以通过Class.forName()方法或ClassLoader.loadClass()方法来实现
- 显式加载常用于动态加载类,根据条件或配置来决定加载哪些类
1.5 链接(Linking)
链接阶段是连接器将类的二进制数据合并到JVM的运行时状态中的过程。链接过程包括验证、准备和解析三个步骤。
1、验证(Verification)
- 验证阶段的目标是确保类的字节码符合JVM规范和安全约束
- 这个步骤会检查字节码的结构、语义以及对其他类的引用是否有效,以确保代码的安全性和正确性
2、准备(Preparation)
- 准备阶段是为类的静态变量在方法区分配内存空间,并设置默认初始值
- 在准备阶段,静态变量被赋予程序中所定义的初始值,而不是实际的赋值操作
- 对于基本类型,会赋予默认的零值(零或false),对于引用类型,会赋予null值
- 对于final修饰的变量来说,会在此时就被初始化为指定值
3、解析(Resolution)
- 解析阶段是将类的符号引用转换为直接引用的过程
- 符号引用是一种编译时的引用,通过符号名称来描述所引用的目
- 直接引用是指直接指向目标的指针、句柄或偏移量。
1.6 初始化(Initialization)
初始化阶段是类加载的最后一个阶段,是执行类的初始化方法(<clinit>方法)的过程。
初始化阶段会按照程序员指定的顺序执行类的静态变量赋值和静态代码块中的代码;初始化阶段只会执行一次,保证了类的静态资源的初始化操作。
初始化阶段利用了一种懒加载的思想,只有当类的主动使用的时才会导致类的初始化。如果是类的被动使用,则不会进行初始化。
当程序中存在以下情况时,会触发对类的主动使用:
- 创建类的实例
- 访问类的静态变量或静态方法
- 使用反射方式对类进行操作
- 初始化一个类的子类
- 启动Java应用程序的入口类(即包含main()方法的类)
当程序中存在以下情况时,会触发对类的被动使用:
- 使用类的静态常量(即final修饰的常量)
- 引用类的静态变量,但不对其进行赋值操作
2. 类加载器
2.1 类加载器概述
类加载器是在类运行时负责将类读到内存的一个对象,其类型为 ClassLoader类型,此类型为抽象类型,通常以父类形式出现。
类加载器对象常用方法说明:
1、getParent() :返回类加载器的父类加载器
2、loadClass(String name):加载名称为 name 的类
3、findClass(String name):查找名称为 name 的类
4、findLoadedClass(String name) :查找名称为 name 的已经被加载过的类
5、defineClass(String name, byte[] b, int off, int len) :把字节数组 b 中的内容转换成 Java 类
2.2 类加载器的层次架构
类加载器的层次架构是指类加载器之间的父子关系,形成了一种层次结构。在Java中,类加载器之间存在三种层次关系:
1、启动类加载器(Bootstrap Class Loader):它是虚拟机的一部分,是最顶层的类加载器。它负责加载Java核心类库,如java.lang包中的类等。启动类加载器是用本地代码实现的,无法通过Java代码直接获取引用。
2、扩展类加载器(Extension Class Loader):它是由Java编写的类加载器,是启动类加载器的子类。它负责加载Java的扩展类库,位于jre/lib/ext目录下的JAR包。扩展类加载器可以通过java.ext.dirs系统属性指定扩展类库的路径。
3、应用程序类加载器(Application Class Loader):也称为系统类加载器,是由Java编写的类加载器,是扩展类加载器的子类。它负责加载应用程序的类,包括开发者自己编写的类和第三方类库。应用程序类加载器可以通过java.class.path系统属性指定类路径。
除了以上三个层次的类加载器,还可以自定义类加载器来满足特定需求。自定义类加载器需要继承ClassLoader类。
2.3 双亲委派机制
双亲委派(Parents Delegation)是指在类加载器加载类时的一种委派机制。根据这个机制,当一个类加载器需要加载某个类时,它会先将加载请求委派给它的父加载器,只有当父加载器无法加载该类时,才由自身来尝试加载。
双亲委派中的双亲,并不是指同级的多个父加载器,而是指可能委派可能涉及多级父加载器。
双亲委派模型的工作过程是:
1、当一个类加载器收到加载请求时,它首先会检查自身是否已经加载了该类。如果已经加载,则直接返回该类的Class对象。
2、如果没有加载,它会将加载请求委派给它的父加载器。父加载器会按照同样的方式进行检查,先检查自身是否已经加载了该类,如果没有加载,再将加载请求委派给它的父加载器。
3、这个过程会一直持续,直到达到最顶层的启动类加载器。
4、如果父类加载失败(它的搜索范围中没有找到所需的类),就会抛出ClassNotFoundException。
5、只有当父加载器反馈自己无法完成这个加载清求时,子加载器才会尝试自己去加载。
这种双亲委派机制的好处是保证了类的唯一性和层次性。由于加载请求总是向上委派,因此在整个类加载器层次结构中,同一个类只会被加载一次,避免了类的重复加载。同时,由于父加载器优先加载类,可以确保核心类库由启动类加载器加载,保证了类的安全性和稳定性。
需要注意的是,双亲委派机制并不是强制性的,可以通过重写类加载器的loadClass()方法来改变默认的委派行为,即“破坏双亲委派机制”。但是,在大多数情况下,推荐遵循双亲委派机制,以确保类加载的一致性和安全性。
3. 总结
1、类加载是Java虚拟机(JVM)将类的字节码加载到内存,并将其转换为可以被JVM执行的对象的过程。
2、类的生命周期指的是从加载到卸载的基本过程。
- 大的阶段可以分为类的加载、类的使用、以及类的卸载
- 类的加载阶段分为加载、链接、初始化三个大的阶段
- 其中链接过程又包含了验证、准备和解析
3、隐式加载和显式加载是类加载过程中的两种加载方式,它们的区别在于触发类加载的时机和方式。
- 隐式加载是指在程序执行过程中,当需要使用某个类时,由JVM自动触发类的加载过程
- 显式加载是指开发者在代码中显式调用类加载器的相关方法来加载指定的类
4、初始化阶段会按照程序员指定的顺序执行类的静态变量赋值和静态代码块中的代码。
- 初始化阶段只会执行一次,保证了类的静态资源的初始化操作
- 初始化阶段利用了一种懒加载的思想,只有当类的主动使用的时才会导致类的初始化
- 如果是类的被动使用,则不会进行初始化
5、类加载器是在类运行时负责将类读到内存的一个对象,其类型为 ClassLoader类型,此类型为抽象类型,通常以父类形式出现。
6、类加载器的层次架构是指类加载器之间的父子关系,形成了一种层次结构,在Java中,类加载器之间存在三种层次关系。
- 启动类加载器(Bootstrap Class Loader)
- 扩展类加载器(Extension Class Loader)
- 应用程序类加载器(Application Class Loader)
- 除了以上三个层次的类加载器,还可以自定义类加载器来满足特定需求
7、双亲委派(Parents Delegation)是指在类加载器加载类时的一种委派机制。
- 根据这个机制,当一个类加载器需要加载某个类时,它会先将加载请求委派给它的父加载器,只有当父加载器无法加载该类时,才由自身来尝试加载