引入
每一个 Java 过程都离不开类的影子,那么类的加载过程是怎么样的呢?
在介绍双亲委派机制的时候,不得不提 ClassLoader。说 ClassLoader 之前,咱们得先理解下 Java 的基本知识。
Java 是运行在 Java 的虚拟机(JVM) 中的,然而它是怎么就运行在 JVM 中了呢?咱们编写的 Java 源代码被编译器编译成 .class 的字节码文件。而后由咱们的 ClassLoader 负责将这些 class 问价加载到 JVM 中去执行。
jvm 的启动是通过疏导类加载器 (Bootstrap classLoader) 创立一个初始类 (initial.class) 来实现的,这个类是由 jvm 的具体实现指定的。
jvm 的组成构造之一就是 类装载器子系统,先来理解一下 Java 代码的执行流程吧。
Java 代码的执行流程
- 首先咱们编写的××.java 文件,会通过 Java 编译器编译,那么这个环节会剖析 java 文件的语法,语义,是否有注解等,接着通过字节码生成器生成一个字节码文件(××.class)
- 而后生成的字节码文件会交由 java 虚拟机解决,java 虚拟机里会进行一些操作,会通过类加载器、字节码校验器、翻译字节码 (解析执行) 和 JIT 编译器 (编译执行) 进行操作
- 之后会返回给操作系统
类加载子系统
类的生命周期
类的生命周期包含:加载、链接、初始化、应用和卸载,其中前三个 (加载、链接、初始化) 属于类加载过程,应用是指咱们 new 出一个对象进行应用,卸载是指对象被垃圾回收掉了。
类加载的过程
loading加载
- 通过类的全限定名(包名 + 类名),获取到该类的。class 文件的二进制字节流
- 将二进制字节流所代表的动态存储构造,转化为办法区运行时的数据结构
- 在内存中生成一个代表该类的 java.lang.class 对象,作为办法区这个类的各种数据的拜访入口
总的来说,就是先加载二进制数据到内存 –> 映射成 jvm 能意识和解决的构造,在内存中生成 class 文件。
Linking链接
链接是指将下面创立好的 class 类合并至 Java 虚拟机中,使之可能执行的过程,可分为 验证 、 筹备 、 解析 三个阶段。
- 验证(Verify)
确保 class 文件中的字节流蕴含的信息,合乎以后虚拟机的要求,保障这个被加载的 class 类的正确性,不会危害到虚拟机的平安。
- 筹备(Prepare)
为类中的 动态字段 分配内存,并设置默认的初始值,比方 int 类型初始值是 0。被 final 润饰的 static 字段不会设置,因为 final 在编译的时候就调配了
- 解析(Resolve)
解析阶段的目标,是将常量池内的符号援用转换为间接援用的过程(将常量池内的符号援用解析成为理论援用)。如果符号援用指向一个未被加载的类,或者未被加载类的字段或办法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化)。
事实上,解析器操作往往会随同着 JVM 在执行完初始化之后再执行。符号援用就是一组符号来形容所援用的指标。符号援用的字面量模式明确定义在《Java 虚拟机标准》的 Class 文件格式中。间接援用就是间接指向指标的指针、绝对偏移量或一个间接定位到指标的句柄。
解析动作次要针对类、接口、字段、类办法、接口办法、办法类型等。对应常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等。
initialization初始化
初始化就是执行类的结构器办法 init()的过程
这个办法不须要定义,是 javac 编译器主动收集类中所有类变量的赋值动作和动态代码块中的语句合并来的
若该类具备父类,jvm 就会保障父类的 init()先执行,而后再执行子类的 init()。
类加载器的分类
- 启动类 / 疏导类:Bootstrap ClassLoader
1. 这个类加载器应用 C /C++ 语言实现的,嵌套在 JVM 外部,java 程序无奈间接操作这个类。
2. 它用来加载 Java 外围类库,如:JAVA_HOME/jre/lib/rt.jar
、resources.jar
、sun.boot.class.path
门路下的包,用于提供 jvm 运行所需的包。
3. 并不是继承自 java.lang.ClassLoader,它没有父类加载器
4. 它加载 扩大类加载器
和应用程序类加载器
,并成为他们的父类加载器
5. 出于平安思考,启动类只加载包名为:java、javax、sun 结尾的类
- 扩大类加载器:Extension ClassLoader
1.Java 语言编写,由 sun.misc.Launcher$ExtClassLoader
实现,咱们能够用 Java 程序操作这个加载器
2. 派生继承自 java.lang.ClassLoader,父类加载器为 启动类加载器
3. 从零碎属性:java.ext.dirs
目录中加载类库,或者从 JDK 装置目录:jre/lib/ext
目录下加载类库。咱们就能够将咱们本人的包放在以上目录下,就会主动加载进来了。
- 应用程序类加载器:Application Classloader
1.Java 语言编写,由 sun.misc.Launcher$AppClassLoader
实现。
2. 派生继承自 java.lang.ClassLoader,父类加载器为 启动类加载器
3. 它负责加载 环境变量 classpath
或者 零碎属性 java.class.path
指定门路下的类库
4. 它是程序中默认的类加载器,咱们 Java 程序中的类,都是由它加载实现的。
5. 咱们能够通过ClassLoader#getSystemClassLoader()
获取并操作这个加载器
- 自定义加载器
1. 个别状况下,下面三种加载器能满足咱们日常的开发工作,不满足时,咱们能够自定义加载器
自定义加载器实现步骤
继承
java.lang.ClassLoader
类,重写 findClass()办法
如果没有太简单的需要,能够间接继承URLClassLoader
类,重写loadClass
办法,具体可参考AppClassLoader
和ExtClassLoader
。
获取 ClassLoader 的几种形式
它是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包含启动类加载器)
// 形式一:获取以后类的 ClassLoader
clazz.getClassLoader()
// 形式二:获取以后线程上下文的 ClassLoader
Thread.currentThread().getContextClassLoader()
// 形式三:获取零碎的 ClassLoader
ClassLoader.getSystemClassLoader()
// 形式四:获取调用者的 ClassLoader
DriverManager.getCallerClassLoader()
类加载机制 - 双亲委派机制
jvm 对 class 文件采纳的是按需加载的形式,当须要应用该类时,jvm 才会将它的 class 文件加载到内存中产生 class 对象。
在加载类的时候,是采纳的双亲委派机制,即把申请交给父类解决的一种工作委派模式。
工作原理
(1)如果一个类加载器收到了类加载申请,它本人不会先去加载,会把这个申请委托给覅类加载器去执行。
(2)如果父类还存在父类加载器,则持续向上委托,始终委托到顶层 (启动类加载器 Bootstrap ClassLoader)
(3) 如果父类加载器能够实现加载工作,就返回胜利后果,如果父类加载失败,就由子类本人去尝试加载,如果子类加载失败就会抛出 ClassNotFoundException
异样,这就是 双亲委派机制
第三方包加载形式:反向委派机制
在 Java 利用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口容许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI 等,这些 SPI 的接口属于 Java 外围库,个别存在 rt.jar 包中,由 Bootstrap 类加载器加载。而 Bootstrap 类加载器无奈间接加载 SPI 的实现类,同时因为双亲委派模式的存在,Bootstrap 类加载器也无奈反向委托 AppClassLoader 加载器 SPI 的实现类。在这种状况下,咱们就须要一种非凡的类加载器来加载第三方的类库,而线程上下文类加载器(双亲委派模型的破坏者)就是很好的抉择。
从图可知 rt.jar 外围包是有 Bootstrap 类加载器加载的,其内蕴含 SPI 外围接口类,因为 SPI 中的类常常须要调用内部实现类的办法,而 jdbc.jar 蕴含内部实现类 (jdbc.jar 存在于 classpath 门路) 无奈通过 Bootstrap 类加载器加载,因而只能委派线程上下文类加载器把 jdbc.jar 中的实现类加载到内存以便 SPI 相干类应用。显然这种线程上下文类加载器的加载形式毁坏了“双亲委派模型”,它在执行过程中摈弃双亲委派加载链模式,使程序能够逆向应用类加载器,当然这也使得 Java 类加载器变得更加灵便。
沙箱平安机制
自定义 String 类,然而在加载自定义 String 类的时候会率先应用疏导类加载器加载,而疏导类加载器在加载的过程中会先加载 JDK 自带的文件(rt.jar 包中的 javalangString.class),报错信息说没有 main 办法就是因为加载的 rt.jar 包中的 String 类。这样能够保障对 Java 外围源代码的爱护,这就是沙箱平安机制。