类加载步骤
何为加载?在计算机领域里,加载表示启动程序时文件或信息的载入。同理,Java程序启动的时候需要将使用到的类从Class文件(字节码文件)中加载到Java虚拟机(以下称为JVM)中去。
JVM加载每个类的时候一般分为3个步骤:
- 加载
- 链接
- 初始化
结构步骤图如下:
步骤一:加载
此步骤主要是通过类加载器来读取类的字节流,将类的静态存储结构转换为方法区的运行时数据结构,在内存中生成一个代表该类的Class对象,作为方法区该类的各种数据的访问接口。
步骤二:链接
此步骤分为3个小步骤,分别为检验、准备和解析。
- 校验:确保class文件的内容符合虚拟机的规范,不会危害自身的安全,校验主要分为四种校验,分别是文件格式校验、元数据校验、字节码校验、符号引用校验。
- 准备:为类变量(静态变量)分配内存空间并设置初始值,需要注意的是,使用final修饰的类变量在编译期间就已经分配空间了,该阶段会直接显式初始化,而实例变量不会在这里分配内存空间和初始化,因为实例变量是会随之对象被分配到JVM堆中的。
- 解析:将符号引用转化为直接引用的过程。符号引用是用一组符号来描述引用的目标,符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中,而直接引用就是直接指向目标的指针、相对偏移量或者一个间接定位到目标的句柄。(实际上解析操作往往实在JVM执行完之后再执行)
步骤三:初始化
此步骤是执行类构造器方法<clinit>()的过程,<clinit>方法将Java编译器自动收集类中的类变量的赋值动作和静态代码块中的语句合并而来,此方法不需要被定义。
在字节码查看器的视角下,构造器方法<clinit>中的指令是按照Class文件中语句出现的顺序执行的。在此角度下,还会存在一个<init>方法,此方法是构造器视角下的方法(可以理解为构造器就是<init>),无论是否显式创建构造器,都会有此方法,因为没有显式创建构造器的时候,Java会隐式的创建一个构造器。而<clinit>方法是类中存在静态属性的赋值或静态代码块的时候才会存在的。
总结
- final修饰的类变量在编译期间分配空间,而在链接步骤(步骤二)的准备阶段就进行显式初始化。
- 无final修饰的类变量在链接步骤(步骤二)的准备阶段分配空间并设置初始值。
- <init>方法一定会存在(任何一个类声明后内部一定存在一个构造器),而<clinit>方法将只有类中存在静态属性的赋值或静态代码块的时候才会存在。
- 如果被加载的类有父类,那么JVM会保证子类的构造器被执行前,父类的构造器已经执行完毕,<init>和<clinit>都一样。
- 虚拟机必须保证一个类的<clinit>方法在多线程下被同步加锁,只会执行一次。
类加载器的分类
在JVM规范中,主要支持两种类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader),因为在概念上,所有派生于抽象类ClassLoader的类加载器都会被划分为自定义类加载器。
实际上,Java加载器可以分为三种,分别是:
- 引导类加载器(Bootstrap ClassLoader),又称启动类加载器
- 扩展类加载器(Extension ClassLoader)
- 系统类加载器(System ClassLoader),又称用户自定义类加载器
关系图如下:
注意:加载器之间的关系不是上下层关系,不是子父类关系,而是包含关系。关系图中存在的User Defined ClassLoader也可以归于系统类加载器,因为用户自定义类都是使用系统类加载器获取的。
引导类加载器
引导类加载器有以下特点:
- 使用C/C++语言编写,嵌套在JVM的内部
- 用于加载核心类库(java/javax/sun开头的类库)
- 没有父类加载器,不继承于java.lang.ClassLoader
- 用来加载扩展类加载器和系统类加载器
- 无法通过代码获取引导类加载器(获得的引用为null)
扩展类加载器
扩展类加载器有以下特点:
- Java语言编写
- 派生与java.lang.ClassLoader类
- 父类为引导类加载器
- 加载目录jre/lib/ext,如果用户自定义的jar包处于此目录也可由扩展类加载器加载
系统类加载器
系统类加载器有以下特点:
- Java语言编写
- 派生于java.lang.ClassLoader类
- 父类为扩展类加载器
- 加载目录classpath或系统,用户自己写的类库都是默认用系统类加载器加载
双亲委派机制
双亲委派机制是Java为了保护核心类库所设计的一种类加载机制。
原理
如果一个类加载器收到了类加载的请求,那么它并不会自己加载,而是把这个请求给父类加载器区加载,如果父类加载器还存在父类,那么会进一步向上委托,依次递归,最终类加载请求会到达引导类加载器,如果引导类加载器能够加载则进行加载,如果不能会向下委托给子类加载器尝试加载,子类加载器无法加载则会给子类加载器的子类加载,依次递归,如果都不能加载就会抛出类找不到的错误,这就是双亲委派机制。
目的
- 避免类的重复加载
- 保护核心类库的正确加载,防止核心API被篡改