类加载机制
类加载时机
一个类从被加载到虚拟机内存中开始,到卸载出内存为止,他的生命周期如下:
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,而解析阶段不一定,在某些情况下在初始化之后再开始,这是为了支持Java语言的运行时绑定(动态绑定)
<aside> 💡 对于初始化阶段,有且只有六种情况必须立即对类进行初始化:
- 遇到new、getstatic、putstatic或者invokestatic这四条字节码指令时,如果没有进行过初始化,则先触发初始化: ·使用new关键字实例化对象的时候。 ·读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。 ·调用一个类型的静态方法的时候。
- 使用反射包的方法对类型进行反射调用的时候,若没有初始化则触发初始化
- 在初始化时,若其父类没有被初始化则先触发父类的初始化
- 在虚拟机启动时,虚拟机会先初始化指定执行的主类(包含main()方法的类)
- 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
- 如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
</aside>
类加载过程
-
加载
在加载阶段,Java虚拟机完成以下三件事:
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口
-
验证
验证的目的是确保Class文件的字节中的信息符合虚拟机规范的全部约束,保证这些信息被当作代码运行后不会危害虚拟机的安全。
-
文件格式验证
验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理
-
元数据验证
对字节码描述的信息进行语义分析,主要目的是对类的元数据信息进行语义校验,保存不存在与语言规范相悖的元数据信息
-
字节码验证
通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的
-
符号引用验证
确保解析行为能正常执行,判断该类是否缺少或被禁止访问它依赖的某些外部类等。
-
-
准备
为定义的变量(静态变量)分配内存并设置类变量初始值(零值)的阶段。概念上讲,类静态变量所使用的内存在方法区中,而在JDK8之后,类变量会随着Class对象一起存放在堆中。
-
解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
-
初始化
初始化阶段是执行初始化方法
<clinit> ()
方法的过程。会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。
类加载器
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
-
双亲委派机制
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 包和类。
这里类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合管理在复用父加载器代码。
双亲委派模型的工作过程是:在类加载的时候,系统会首先判断当前类是否被加载过。若需要加载,类加载不会自己进行加载而是把加载的请求委托给父亲加载器的
loadClass()
来处理,因此所有的请求都会传递到BootStapClassLoader
中。当父亲类加载器无法处理时,子加载器才会尝试自己去加载。 - BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,负责加载
-
破坏双亲委派机制
自定义加载器的话,需要继承
ClassLoader
。如果我们不想打破双亲委派模型,就重写ClassLoader
类中的findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写loadClass()
方法。有了线程上下文类加载器,可以实现父类加载器去请求子类加载器完成类加载的行为。Java中涉及
SPI
的加载基本上都采用这种方法来完成 -
双亲委派的好处
双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载,也保证了 Java 的核心 API 不被篡改。