类的加载流程
类的加载流程包含:加载、验证、准备、解析、初始化
加载
作为第一个阶段,虚拟机需要完成了以后三个步骤
- 通过一个类的全限类名来获取定义的二进制字节流
- 将这个字节流所存储的静态结构转化为方法区运行时候的数据结构
- 在java堆中,生成一个代表这个类的java.lang.Class对象,最为对方法区中这些数据的访问入口
验证
确保被加载类的正确性,包含检测以下信息
- 文件格式的验证:验证字节流是否符合Class文件格式的规范
- 元数据的验证::对字节码描述的信息进行语义分析
- 字节码的验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:确保解析动作能正确执行。
准备
为类的静态变量分配内存,并将其初始化默认值
- 这个时候进行内存分配的仅是类变量(static),而不包括实例变量,实例变量会在对象实例化的时候,随着对象一块分配到堆中
- 这的初始化默认值,通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是指的是显示赋值
解析
将类中的符号引用转换为直接引用
解析阶段就是虚拟机将常量池内符号引用替换为直接引用,解析的动作主要是针对类的接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
- 符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
- 直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
初始化
为类的静态变量赋予正确的初始值,到了初始阶段,才开始真正执行类中定义的Java程序代码。
- 声明类变量是指定初始值
- 使用静态代码块为类变量指定初始值
类的加载器
示例图
==注意:这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的==
在java虚拟机的角度来说只存在两种不同的类加载器
- 启动类的加载器:使用C++实现
- 所有其他类的加载器:使用java语言实现的,独立于虚拟机之外,并且全部继承自抽象类java.lang.Classlocad,这些类的加载器需要由启动类加载器加载到内存中之后才能去加载其他类
在开发者的角度来说,类的加载器可以大致划分为三类
- 启动类加载器
- 扩展类加载器
- 应用程序类加载器
类加载器的加载机制
- 全盘负责:当一个类加载器负责加载某Class时候,该Class依赖的Class和引用的Classs也将由该类加载器负责载入,除非显示的使用另一个类加载器来载入
- 父类委托:先让父类加载器试图加载该类,只有在父类加载器无法加载该类的时候,才尝试从自己的类路径中加载该类
- 缓存机制:缓存机制将保证所有加载过的Class都会被缓存,当程序中需要使用某一个Class,类加载器会现在缓存中查找该Class,只要缓存不存在的时候,系统才会读取该类对应的二进制数据,将其转化成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
类的加载
类加载的方式
- 命令行启动应用时候由JVM初始化加载
- 通过Class.forName()方法动态加载:将类的.class文件加载到jvm中,并会对类进行解释,执行类的static块
- 通过ClassLoader.loadClass()方法动态加载:只做一件事,将类的.class文件加载到jvm中,不会执行static块,只有在newInstance才会去执行static块
双亲委派机制
双亲委派机制的流程:如果一个类加载器收到了一个类加载的请求,首先他不会尝试自己加载该类,而是把这个请求委托给父类加载器加载该类,依次向上传递,因此,所有的类记载请求都会请求到启动类加载器中,父类加载器在他的搜索范围内没有找到所需要的类时,即无法完成该类的加载,子加载器才会尝试自己去加载该类,如果还没有找到,那么直接抛出异常
示例流程详解
- 当 AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
- 当 ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
- 如果 BootStrapClassLoader加载失败(例如在 $JAVA_HOME/jre/lib里未查找到该class),会使用 ExtClassLoader来尝试加该类
- 若ExtClassLoader也加载失败,则会使用 AppClassLoader来加载,如果 AppClassLoader也加载失败,则会报出异常 ClassNotFoundException。
双亲委派机制的意义
- 系统类防止内存中出现多份同样的字节码
- 保证Java程序安全稳定运行
代码示例
// 首先判断该类型是否已经被加载 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载 if (parent != null) { //如果存在父类加载器,就委派给父类加载器加载 c = parent.loadClass(name, false); } else { //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name) c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader }
自定义的类加载器
通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自 ClassLoader类,从上面对 loadClass方法来分析来看,我们只需要重写 findClass 方法即可。
自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密