JVM学习笔记——类加载过程
类加载模型——双亲委派模型(Parents Delegation Model)也可称为“溯源委派加载模型”
Java的类加载器是一个运行时核心基础设施模块,主要是启动之初进行类的Load、Link和Init,即加载、链接、初始化。
第一步,Load阶段读取类文件产生二进制流,并转化为特定的数据结构,初步校验cafe babe魔法数、常量池、文件长度、是否有父类等,然后创建对应类的java.lang.Class实例。
第二步,Link阶段包括验证、准备、解析三个步骤。验证是更详细的校验,比如final是否合规、类型是否正确、静态变量是否合理等;准备阶段是为静态变量分配内存,并设定默认值,解析类和方法确保类与类之间的相互引用正确性,完成内存结构布局。
第三步,Init阶段执行类构造器<clinit>方法,如果赋值运算是通过其他类的静态方法来完成的,那么会马上解析另外一个类,在虚拟机栈中执行完毕后通过返回值进行赋值。
类加载是一个将.class字节码文件实例化成Class对象并进行相关初始化的过程。在这个过程中,JVM会初始化继承树上还没有被初始化过的所有父类,并且会执行在这个链路上所有未执行过的静态代码块、静态变量赋值语句等。某些类在使用时,也可以按需由类加载器进行加载。
类加载器类似于原始部落结构,存在权力等级制度。最高的一层是家族中威望最高的Bootstrap,它是在JVM启动时创建的,通常由与操作系统相关的本地代码实现,是最根基的类加载器,负责装载最核心的Java类,比如Object、System、String等;第二层是在JDK9版本中,称为Platform ClassLoader,即平台类加载器,用以加载一些扩展的系统类,比如XML、加密、压缩相关的功能等,JDK9之前的加载器是Extension ClassLoader;第三层是Application ClassLoader的应用类加载器,主要是加载用户定义的CLASSPATH路径下的类。第二、三层类加载器为Java语言实现,用户也可以自定义类加载器。查看本地类加载器的方式如下:
//正在使用的类加载器
ClassLoaer c = TestWhoLoad.class.getClassLoader(); //AppClassLoader 的父加载器是PlatformClassLoader
ClassLoader c1 = c.getParent(); //PlatformClassLoader的父加载器是Bootstrap。它是使用C++来实现的,返回null
ClassLoader c2 = c1.getParent();
AppClassLoader的Parent为Bootstrap,它是通过C/C++实现的,并不存在于JVM体系内,所以输出为null,类加载器具有等级制度,但是并非继承关系,以组合的方式来复用父加载器的功能,这也符合组合优先原则。
低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类。如果低层次的类加载器想加载一个未知类,要非常礼貌地向上级逐级询问:‘’请问,这个类已经加载了吗?“被询问的高层次类加载器会自问两个问题:第一,我是否已经加载过此类?第二,如果没有,是否可以加载此类?只有当所有高层次类加载器在两个问题上的答案均为“否”时,才可以让当前类加载器加载这个未知类。
什么时候需要自定义类加载器呢?
1)隔离加载类。在某些框架内进行中间件与应用模块隔离,把类加载到不同的环境。比如,阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。
2)修改类加载方式。类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况再某个时间点进行按需进行动态加载。
3)扩展加载源。比如数据库、网络,甚至是电视机机顶盒进行加载。
4)防止源码泄露。Java代码容易被编译和篡改,可以进行编译加密。那么类加载器也需要自定义,还原加密的字节码。
实现自定义类加载器的步骤:继承ClassLoader,重写findClass()方法,调用defineClass()方法。一个简单的类加载器实现的示例代码如下:
public class CustomClassLoader extends ClassLoader { @Override
protected Class<?> findClass(String name) throws ClassNotFoundException { try {
byte[] result = geteClassFromCustomPath(name);
if (result == null) {
throw new FileNotFoundException();
} else {
return defineClass(name, result, 0, result.length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
} private byte[] geteClassFromCustomPath(String name) {
//从自定义路径中加载指定类 } public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader();
try {
Class<?> clazz = Class.forName("One", true, customClassLoader);
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
由于中间件一般都有自己的依赖jar包,在同一个工程内引用多个框架时,往往*进行类的仲裁。按某种规则jar包的版本被统一指定,导致某些类存在包路径、类名相同的情况,就会引发类冲突,导致应用程序出现异常。主流的容器类框架都会自定义类加载器,实现不同中间件之间的类隔离,有效避免了类冲突。
参考——《码出高效》 第四章走进JVM