JVM(二)-- 类加载机制
类加载机制
官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html
Loading
ClassLoader
双亲委派机制
(1)检查某个类是否已经加载
自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个Classloader已加载,就视为已加载此类,保证此类只所有ClassLoader加载一次。
(2)加载的顺序
自顶向下,也就是由上层来逐层尝试加载此类。
代码示例
public static void main(String[] args) {
//AppClassLoader
System.out.println(new User().getClass().getClassLoader());
//ExtClassLoader
System.out.println(new User().getClass().getClassLoader().getParent());
//Bootstrap ClassLoader 是c++写的 所以这里打印出来为null
System.out.println(new User().getClass().getClassLoader().getParent().getParent());
//这里也为null String是在Bootstrap ClassLoader中加载的
System.out.println(new String().getClass().getClassLoader());
}
破坏双亲委派
(1)第一次破坏
在 jdk 1.2 之前,那时候还没有双亲委派模型,不过已经有了 ClassLoader 这个抽象类,所以已经有人继承这个抽象类,重写 loadClass 方法来实现用户自定义类加载器。
而在 1.2 的时候要引入双亲委派模型,为了向前兼容, loadClass 这个方法还得保留着使之得以重写,新搞了个 findClass 方法让用户去重写,并呼吁大家不要重写 loadClass 只要重写 findClass。
这就是第一次对双亲委派模型的破坏,因为双亲委派的逻辑在 loadClass 上,但是又允许重写 loadClass,重写了之后就可以破坏委派逻辑了
(2)tomcat
Tomcat就是用这种方式对双亲委派进行破坏,来达到使用一个web容器部署两个或者多个应用程序,不同的应用程序,可能依赖同一个第三方类库的不同版本,还要保证每一个应用程序的类库都是独立、相互隔离的效果。
tomcat自定义类加载器,重写loadClass方法使其优先加载自己目录下的class文件,来达到class私有的效果。
(3)基于SPI
例如:JNDI服务
它的代码是启动类加载器去加载的,JNDI的目的是为了对资源进行集中管理与查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,但启动类加载器显然不会知道这些代码。
由此,Java的设计团队引入了一个不怎么优雅的设计:线程上下文类加载器;这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,在创建线程时如果还未设置的话,
他会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那么这个类加载器默认就是应用程序类加载器。
这样,JNDI服务就可以去加载它所需要的SPI代码,但这种打通了双亲委派模型层次结构由此来逆向使用类加载器的行为,实际上就已经违背了双亲委派模型的一般性原则!
(4) OSGI (开放服务网关协议,Open Service Gateway Initiative)
OSGI 就是利用自定义的类加载器机制来完成模块化热部署,而它实现的类加载机制就没有完全遵循自下而上的委托,有很多平级之间的类加载器查找。
(5)第五次破坏
在 JDK9 引入模块系统之后,类加载器的实现其实做了一波更新。
像扩展类加载器被重命名为平台类加载器,核心类加载归属了做了一些划分,平台类加载器承担了更多的类加载,上面提到的 -Xbootclasspath、java.ext.dirs 也都无效了,rt.jar 之类的也被移除,
被整理存储在 jimage 文件中,通过新的 JRT 文件系统访问。
当收到类加载请求,会先判断该类在具名模块中是否有定义,如果有定义就自己加载了,没的话再委派给父类。
双亲委派被破坏的补救措施
java.lang.ClassLoader的loadClass方法在Java很早的版本就有了,而双亲委派模型是在JDK1.2中引入的。java是向下兼容的,所以不是不想改而是改不了。一个补救措施是使用findClass方法而不是直接重写loadClass。
Linking
Verification
保证被加载类的正确性
Preparation
为类的静态变量分配内存,并将其初始化为默认值
Resolution
把类中的符号引用转换为直接引用
Initialization
对类的静态变量,静态代码块执行初始化操作。
面试题
使用static + final 修饰的字段的显示赋值的操作 ,到底在哪个阶段进行的赋值?
类变量初始化(static final)两种情况:
-
情况:在链接阶段的准备环节赋值
对于基本数据类型的字段来说,如果使用static final修饰,则显示赋值(直接赋值常量,而非调用方法)通常是在链接阶段的准备环节进行
对于String来说,如使用字面量的方式赋值,使用static final 修饰的话,则显示赋值通常是在链接阶段的准备环节进行
-
情况:在初始化阶段
() 中赋值 排除上述的在准备环节赋值的情况之外的情况
public class InitialzationTest{
public static int a = 1;//在初始化阶段<clinit>() 中赋值
public static final int b = 10;//在链接阶段的准备环节赋值
public static final int f = new Random().nextInt();//在初始化阶段<clinit>() 中赋值
public static Integer c = Integer.valueOf(100);//在初始化阶段<clinit>() 中赋值
public static final Integer d = Integer.valueOf(100);//在初始化阶段<clinit>() 中赋值
public static String s2 ="helloworld2";//在初始化阶段<clinit>() 中赋值
public static final String s0 = "helloworld0";//在链接阶段的准备环节赋值
public static final String s1 = new String("helloworld1");//在初始化阶段<clinit>() 中赋值
}
结论:字段使用static + final 修饰,数据类型为基本数据类型或String类型,且显示赋值不涉及方法或构造器调用,赋值操作都是在链接阶段的准备环节进行。