类加载过程
类的生命周期
加载 → 连接 → 初始化 → 使用过程→ 卸载
类的加载就是前三步
其中连接又可分为三步 : 验证 → 准备→ 解析
现在我们对整个类加载过程进行分析
加载
类的加载需要完成三件事:
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口
一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass() 方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。
验证
首先我们会验证文件的格式是否合法,也就是是否符合Class文件格式的规范,再对元数据进行验证,确保其描述的信息符合Java语言规范的要求,然后对字节码进行验证,该阶段用来确定程序语义是合法的、符合逻辑的。最后是符号引用验证,以确保解析指令动作能够正确执行。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
需要注意的是:
- 这时候进行内存分配的仅包括类变量( Class Variables ,即静态变量,被
static
关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。 - 从概念上讲,类变量所使用的内存都应当在 方法区 中进行分配。但是由于一些变动在JDK7之后,类变量则会随着Class对象一起存放在Java堆中。
- 这里所设置的初始值"通常情况"下是数据类型默认的零值。并非我们赋予的值。但是当变量被final修饰时就会在这里进行初始化。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。
- 符号引用就是一组符号来描述目标,可以是任何字面量。
- 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化
初始化阶段是执行初始化方法 <clinit> ()
方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。
<clinit> ()
方法是编译之后自动生成的。该方法是带锁线程安全的。所以在多线程环境下进行类的初始化可能引起多个线程阻塞。
必须对类进行初始化的六大情况:
- 当遇到
new
、getstatic
、putstatic
或invokestatic
这 4 条直接码指令时 - 使用
java.lang.reflect
包的方法对类进行反射调用时 - 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化
- 当虚拟机启动时,用户需要定义一个要执行的主类 (包含
main
方法的那个类),虚拟机会先初始化这个类 MethodHandle
和VarHandle
可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用findStaticVarHandle
来初始化要调用的类- 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。(其实这就类似第三点,当然这是个人推测)。
卸载
卸载类即该类的 Class 对象被 GC。
卸载类需要满足 3 个要求:
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被 GC
由第三点我们可以知道:在JVM生命周期内,由JVM自带的类加载器加载的类是不会被卸载的。但是我们自定义的类加载器加载的类是可能被卸载的。
类加载器
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
:
- BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,负责加载
%JAVA_HOME%/lib
目录下的 jar 包和类或者被-Xbootclasspath
参数指定的路径中的所有类。 - ExtensionClassLoader(扩展类加载器) :主要负责加载
%JRE_HOME%/lib/ext
目录下的 jar 包和类,或被java.ext.dirs
系统变量所指定的路径下的 jar 包。 - AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
双亲委派模型
首先我们来讲一下双亲委派模型中的“双亲”,这里的双亲并不是通过继承关系来确定的,而是通过类加载器的优先级来确定的。双亲就是比该类加载器优先级更高一层的类加载器。
这里介绍一下具体的优先级(由高到低):
BootstrapClassLoader(启动类加载器) > ExtensionClassLoader(扩展类加载器)>AppClassLoader(应用程序类加载器) >用户自定义类加载器
然后我们来学习一下该模型的具体操作流程:
在加载类的时候,系统会自底向上逐层检查该类是否被加载过,已经被加载的类会直接返回,否则就是尝试加载。加载的时候则是自顶向下尝试加载,只有高级类加载器无法加载才下移到低一级类加载器加载。当父类类加载器为null时,则是启用BootstrapClassLoader 做为父类类加载器。