Java 类加载过程概述
在 Java 中,类装载器把一个类装入 Java 虚拟机中,要经过三个步骤来完成:装载、链接和初始化,其中链接又可以分成校验、准备、解析
Java类加载过程分为如下步骤:
1.装载( 加载):查找和导入类或接口的二进制数据;通过类的全限定名获取类的二进制字节流并存储在内存中。
2.链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;
校验(Verification):检查导入类或接口的二进制数据的正确性;验证字节流的正确性,包括文件格式、语法正确性等。
准备:给类的静态变量分配并初始化存储空间;为类的静态变量分配内存空间,并初始化为默认值。
解析:将符号引用转成直接引用;将符号引用解析为直接引用,例如将类或接口的全限定名解析为内存地址
初始化(Initialization):激活类的静态变量,初始化 Java 代码和静态 Java 代码块,执行类的初始化代码,包括静态变量的赋值和静态代码块的执行等
上述过程中,加载、验证、准备、解析是类加载的前期准备工作,而初始化阶段是类加载的最后一步,也是类真正被使用前的准备工作。
Java的类加载过程详解
Java的类加载过程是一个重要的概念,它涉及到JVM(Java虚拟机)如何动态地加载Java类到内存中,以便执行其中的代码。这个过程大致可以分为三个阶段:加载(Loading)、链接(Linking)和初始化(Initialization)。
- 加载(Loading)
加载是类加载过程的第一个阶段。在这个阶段,JVM主要完成以下三件事情:
通过一个类的全限定名来获取定义此类的二进制字节流:这个二进制字节流可以从文件系统、网络或其他来源获取。将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构:在加载阶段,JVM会在方法区中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。 - 链接(Linking)
链接阶段又可以细分为三个阶段:验证(Verification)、准备(Preparation)和解析(Resolution)。
2.1 验证(Verification)
验证是确保被加载的类的正确性和安全性的阶段。主要包括以下四个方面的验证:
文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的JVM处理。
元数据验证:对类的元数据信息进行语义校验(比如这个类是否有父类,指定的父类是否存在等)。字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证:确保解析动作能正确执行。
2.2 准备(Preparation)
准备阶段是为类的静态变量分配内存,并将其初始化为默认值。这里需要注意的是,这个阶段只是为静态变量分配了内存并设置为了默认值,而真正的赋值操作会在初始化阶段进行。
2.3 解析(Resolution)
解析阶段是JVM将常量池内的符号引用替换为直接引用的过程。符号引用是一个逻辑引用,它只包含了引用的名称和描述,而没有具体的内存地址。而直接引用则是一个指向具体内存地址的指针或者句柄。 - 初始化(Initialization)
初始化阶段是类加载过程的最后一个阶段。在这个阶段,JVM才真正开始执行类中定义的Java代码(比如静态代码块、为静态变量赋值等)。如果在初始化阶段出现了异常,那么JVM会立即停止对该类的初始化,并且不会再次尝试对该类进行初始化。
需要注意的是,类的初始化只会被执行一次。如果一个类已经被加载、链接并且初始化过了,那么再次请求加载这个类时,JVM会直接返回代表这个类的Class对象,而不会重新执行类的加载、链接和初始化过程。这就是所谓的“双亲委派模型”中的一部分。
委托模型机制
委托模型机制的工作原理很简单:当类加载器需要加载类的时候,先请示其Parent(即上一层加载器)在其搜索路径载入,如果找不到,才在自己的搜索路径搜索该类。这样的顺序其实就是加载器层次上自顶而下的搜索,因为加载器必须保证基础类的加载。之所以是这种机制,还有一个安全上的考虑:如果某人将一个恶意的基础类加载到jvm,委托模型机制会搜索其父类加载器,显然是不可能找到的,自然就不会将该类加载进来。
我们可以通过这样的代码来获取类加载器
ClassLoader loader = ClassName.class.getClassLoader(); ClassLoader ParentLoader = loader.getParent();
注意一个很重要的问题,就是Java在逻辑上并不存在BootstrapKLoader的实体!因为它是用C++编写的,所以打印其内容将会得到null。
什么是双亲委派模型(Parent-Delegation Model)
双亲委派机制:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载
所谓双亲委派机制,或者叫父级委托模型,就是指按照类加载器的层级关系JVM 中加载类机制采用的是双亲委派模型,顾名思义,在该模型中,子类加载器收到的加载请求,不会先去处理,而是先把请求委派给父类加载器处理,当父类加载器处理不了时再返回给子类加载器加载;
因为安全。使用双亲委派模型来组织类加载器间的关系,能够使类的加载也具有层次关系,这样能够保证核心基础的 Java 类会被根加载器加载,而不会去加载用户自定义的和基础类库相同名字的类,从而保证系统的有序、安全。
- BootStrap ClassLoader 启 动ClassLoader 启动类加载器
- Extension ClassLoader 扩 展ClassLoader 扩展类加载器
- App ClassLoader 应 ⽤ClassLoader/系 统ClassLoader 应用程序类加载器
- Custom ClassLoader ⾃定义ClassLoader 自定义类加载器
除了BootStrap ClassLoader,每个ClassLoader都有⼀个Parent作为⽗亲。 - 自底向上检查类是否已经加载;
- 自顶向下尝试加载类。
双亲委派机制:当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
Java的类加载器有三个,对应Java的三种类:
Bootstrap Loader // 负责加载系统类 (指的是内置类,像是String,对应于C#中的System类和C/C++标准库中的类)
ExtClassLoader // 负责加载扩展类(就是继承类和实现类)
-AppClassLoader // 负责加载应用类(程序员自定义的类)
好处:安全,避免重复加载
Bootstrap 类加载器负责加载 rt.jar 中的 JDK 类文件,它是所有类加载器的父加载 器 。 Bootstrap 类 加 载 器 没 有 任 何 父 类 加 载 器 , 如 果 你 调 用String.class.getClassLoader() , 会 返 回 null , 任 何 基 于 此 的 代 码 会 抛 出NullPointerException 异常。Bootstrap 加载器被称为初始类加载器。
而 Extension 将加载类的请求先委托给它的父加载器,也就是 Bootstrap,如果没有成功加载的话,再从 jre/lib/ext 目录下或者 java.ext.dirs 系统属性定义的目录下加载类。Extension 加载器由sun.misc.Launcher E x t C l a s s L o a d e r 实现。第三种默认的加载器就是 A p p l i c a t i o n 类加载器了。它负责从 c l a s s p a t h 环境变量中加载某些应用相关的类, c l a s s p a t h 环境变量通常由 − c l a s s p a t h 或 − c p 命令行选项来定义,或者是 J A R 中的 M a n i f e s t 的 c l a s s p a t h 属性。 A p p l i c a t i o n 类加载器是 E x t e n s i o n 类加载器的子加载器。通 s u n . m i s c . L a u n c h e r ExtClassLoader 实现。 第三种默认的加载器就是 Application 类加载器了。它负责从 classpath 环境变量中加载某些应用相关的类,classpath 环境变量通常由-classpath 或-cp 命令行选项来定义,或者是 JAR 中的 Manifest 的 classpath 属性。Application 类加 载 器 是 Extension 类 加 载 器 的 子 加 载 器 。 通 sun.misc.Launcher ExtClassLoader实现。第三种默认的加载器就是Application类加载器了。它负责从classpath环境变量中加载某些应用相关的类,classpath环境变量通常由−classpath或−cp命令行选项来定义,或者是JAR中的Manifest的classpath属性。Application类加载器是Extension类加载器的子加载器。通sun.misc.LauncherAppClassLoader 实现
两种方式来破坏双亲委派模型
第一种,集成ClassLoader抽象类,重写loadClass方法,在这个方法可以自定义要加载的类使用的类加载器。
第二种,使用线程上下文加载器,可以通过java.lang.Thread类的setContextClassLoader()方法来设置当前类使用的类加载器类型。
为什么需要双亲委派模型
在这里,先想一下,如果没有双亲委派,那么用户是不是可以自己定义一个java.lang.Object的同名
类,java.lang.String的同名类,并把它放到ClassPath中,那么类之间的比较结果及类的唯一性将无法
保证,因此,为什么需要双亲委派模型?防止内存中出现多份同样的字节码