类加载的流程
- 加载:将.class文件读取到jvm内存中;
- 验证:确保加载进来的字节流符合JVM规范;
- 准备:为静态变量在方法区分配内存,并设置默认初始值。就是还没有给值,全部赋初始值null或者0,准确的是申请了清空的内存空间;
- 解析:虚拟机将常量池内的符号引用替换为直接引用的过程,依据"="后面计算进行复制;
- 初始化:根据程序中的赋值语句主动为类变量赋值。
分类
Java中的类加载器可以分为两种:
- 系统类加载器
- 自定义类加载器
系统类加载器
Bootstrap ClassLoader(启动类加载器)
主要负责/jre/lib目录下的jar -Xbootclasspath:可以改变其默认加载的目录
C++实现的,所以Java代码中是无法引用的
Extensions ClassLoader (扩展类加载器)
主要负责/jre/lib/ext目录下的jar
-Djava.ext.dir:可以改变其加载的目录
App ClassLoader(系统类加载器)
主要负责Classpath目录下的的所有jar和Class文件,程序中的默认类加载器。
-Djava.class.path:可以修改其加载目录
自定义类加载器
有时候我们需要加载一个类,但是这个类是目前的类加载体系无法访问到的,这时候就要使用自定义的类加载器来加载这个类。
为了可以从指定的目录下加载jar包或者class文件,我们可以用继承java.lang.ClassLoader类的方式来实现一个自己的类加载器。
在开发自己的类加载器时,最好 覆写findClass()方法, 不要覆写 loadClass() 方法。(findClass()会在loadClass()中被调用,loadClass()其实就是双亲委托机制的具体实现)
先要了解下ClassLoader:
- ClassLoader是一个抽象类,定义了ClassLoader的主要功能。
- SecureClassLoader:拓展了ClassLoader类加入了权限方面的功能,加强了ClassLoader的安全性。
- URLClassLoader:提供用来通过URl路径从jar文件和文件夹中加载类和资源功能。
- ExtClassLoader和AppClassLoader都继承自URLClassLoader,它们都是Launcher.class的内部类。
- Launcher 是Java虚拟机的入口应用,ExtClassLoader和AppClassLoader都是在Launcher中进行初始化的。 Bootstrap ClassLoader不在这继承体系中,因为他不是java的因此,他也没法在java代码中引用。
class NetworkClassLoader extends ClassLoader { public Class findClass(String name) { //复写 } }
双亲委托机制
当前类加载器总是将加载交给父级去完成。
记得有2步骤: 向上检查,从下加载
当需要加载某个类的时候,先从当前类加载器先判断该加载器是否加载了该class。如果没有找到就去parent判断是否已经加载。
如果到头了都没有加载过,那么就从头开始让类加载器去他们负责的区域找到该类。
为什么要双亲委派
- 防止重复加载类
- 如果已经加载过一次Class,就不需要再次加载,而是先从缓存中直接读取。(委托的流程)
- 为了安全(避免核心类篡改)
- 比如我复写了一个String类,我想直接替代jdk里面的(不是简单的说我写个包,包里面有个String类那种替代)。因为类加载机制,我们要加载util.String类的时候永远都会交给父级,所以我们写的那个类永远都不会加载到。
- 再比如能不能自己写个类叫java.lang.System?一般情况下当然不能!除非使用自定义类加载器进行加载。
jvm如何认定两个对象同属于一个类型
类的全限定类名+类加载器来唯一标识一个类。 也就是说一个类加载器不能加载同一个类。但是不同的类加载器可以加载相同的类。
破坏双亲委派模型
因为双亲委派模型的缺陷:越基础的类由越上层的加载器进行加载(所谓基础类就是它们总是作为被用户代码调用的API)。
所以有个问题: 如果基础类又要调用回用户的代码,该怎么处理?
以JDBC举例:
DriverManager由BootStrap类进行加载的(处于jdk中,SPI提供的接口)。
但是该Driver具体的实现类是数据库服务商提供的,会由App加载器来加载。这时候我们加载这些驱动的时候,会委托到Bootstrap来加载驱动,但是BootStrap是无法找到这些驱动对应的实现类的。这时候就要破坏这个委托模型,需要启动类加载器来委托子类来加载Driver实现。
怎么解决?
为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
什么是SPI
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。
说白了就是jdk提供的一套接口规范,然后服务方会有一套自己的实现机制。比如jdbc接口,各个数据库厂商针对接口开发各自的驱动来实现扩展。(类似于IOC)
使用场景
- 数据库驱动加载接口实现类的加载,JDBC加载不同类型数据库的驱动
- 日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类
- Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
- Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口
面试题(加深对类加载流程理解)
class Singleton{
// 2段代码的区别就在于 这三行代码的顺序。构造函数是在前面调用的还是在后面调用的,当然value1和value2都是静态的
private static Singleton singleton = new Singleton();
public static int value1;
public static int value2 = 0;
//在类准备阶段value1,value2都初始化好了默认值为0。然后初始化阶段就是为了将=号的赋值来初始化之前的默认值。这时候是程序执行的
//1.先执行构造:value1=1 value2=1
//2.继续value2的赋值操作:value2=0
//结果就是1,0
private Singleton(){
value1++;
value2++;
}
public static Singleton getInstance(){
return singleton;
}
}
class Singleton2{
public static int value1;
public static int value2 = 0;
private static Singleton2 singleton2 = new Singleton2();
//先初始值都是0
//执行赋值value2=0,还是0
//执行构造value2=1.value1=1
//所以最后结果是1,1
private Singleton2(){
value1++;
value2++;
}
public static Singleton2 getInstance2(){
return singleton2;
}
}
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("Singleton1 value1:" + singleton.value1); //1
System.out.println("Singleton1 value2:" + singleton.value2); //0
Singleton2 singleton2 = Singleton2.getInstance2();
System.out.println("Singleton2 value1:" + singleton2.value1); //1
System.out.println("Singleton2 value2:" + singleton2.value2); //1
}